├── .gitignore ├── LICENSE.md ├── docker ├── .env ├── docker-compose-automated.yml ├── docker-compose.yml ├── gw-build │ ├── Dockerfile │ ├── base.gwbk │ ├── docker-entrypoint-shim.sh │ ├── register-module.sh │ ├── register-password.sh │ └── retrieve-modules.sh ├── gw-init │ └── git.conf ├── gw-secrets │ ├── GATEWAY_ADMIN_PASSWORD │ └── GATEWAY_GIT_USER_SECRET └── readme.md ├── git-build ├── license.html └── pom.xml ├── git-client ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── axone_io │ └── ignition │ └── git │ ├── ClientHook.java │ └── ClientScriptModule.java ├── git-common ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── axone_io │ │ └── ignition │ │ └── git │ │ ├── AbstractScriptModule.java │ │ └── GitScriptInterface.java │ └── resources │ └── com │ └── axone_io │ └── ignition │ └── git │ └── AbstractScriptModule.properties ├── git-designer ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── axone_io │ │ └── ignition │ │ └── git │ │ ├── CommitPopup.form │ │ ├── CommitPopup.java │ │ ├── DesignerHook.java │ │ ├── actions │ │ └── GitBaseAction.java │ │ ├── components │ │ └── SelectAllHeader.java │ │ ├── managers │ │ └── GitActionManager.java │ │ └── utils │ │ └── IconUtils.java │ └── resources │ └── com │ └── axone_io │ └── ignition │ └── git │ ├── DesignerHook.properties │ └── icons │ ├── ic_commit.svg │ ├── ic_folder.svg │ ├── ic_git.svg │ ├── ic_pull.svg │ ├── ic_push.svg │ ├── ic_unregister_user.svg │ └── ic_verified_user.svg ├── git-gateway ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── axone_io │ │ └── ignition │ │ └── git │ │ ├── GatewayHook.java │ │ ├── GatewayScriptModule.java │ │ ├── SshTransportConfigCallback.java │ │ ├── commissioning │ │ ├── GitCommissioningConfig.java │ │ └── utils │ │ │ └── GitCommissioningUtils.java │ │ ├── managers │ │ ├── GitImageManager.java │ │ ├── GitManager.java │ │ ├── GitProjectManager.java │ │ ├── GitTagManager.java │ │ └── GitThemeManager.java │ │ ├── records │ │ ├── GitProjectsConfigRecord.java │ │ └── GitReposUsersRecord.java │ │ └── web │ │ ├── GitProjectsConfigEditPage.java │ │ ├── GitProjectsConfigPage.java │ │ ├── GitReposUsersEditPage.java │ │ ├── GitReposUsersPage.java │ │ ├── ProjectList │ │ ├── ProjectListEditorSource.java │ │ └── ProjectSourceEditor.java │ │ └── component │ │ └── CustomRecordListModel.java │ └── resources │ └── com │ └── axone_io │ └── ignition │ └── git │ ├── bundle_git.properties │ ├── records │ ├── GitProjectsConfigRecord.properties │ └── GitReposUsersRecord.properties │ └── web │ ├── GitProjectsConfigPage.properties │ └── ProjectList │ └── ProjectSourceEditor.html ├── img ├── CommitPopup.png ├── GitStatusBar.png └── GitToolbar.png ├── pom.xml └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # A general .gitignore for Gradle or Maven built Ignition SDK projects that 2 | # use IntelliJ or Eclipse as an IDE 3 | 4 | # Ignition Module files 5 | *.modl 6 | 7 | # Java class files 8 | *.class 9 | 10 | # generated files 11 | bin/ 12 | gen/ 13 | 14 | # Local configuration file used for proj. specific settings (sdk paths, etc) 15 | local.properties 16 | 17 | # Eclipse project files 18 | .classpath 19 | .project 20 | 21 | # Intellij project files 22 | *.iml 23 | *.ipr 24 | *.iws 25 | .idea/ 26 | 27 | # git repos 28 | .git/ 29 | 30 | # hg repos 31 | .hg/ 32 | 33 | # Maven related 34 | */target/ 35 | *.versionsBackup 36 | 37 | # Gradle related files and caches 38 | .gradletasknamecache 39 | .gradle/ 40 | build/ 41 | 42 | # Module Signer 43 | module-signer/ 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Git Module License 2 | 3 | ## THE BEERWARE LICENSE (Revision 42): 4 | 5 | Enzo Sagnelonge, working for AXONE-IO, wrote this code.
6 | As long as you retain this notice, you can do whatever you want with this stuff.
7 | If we meet someday, and you think this stuff is worth it, you can buy me a beer in return.
8 | 9 | ## Third-Party Licenses : 10 | - This software uses the JGit library, which is distributed under the Eclipse Distribution License (New BSD License). 11 | - This software uses the Intellij forms_rt library, which is distributed under the Apache License 2. 12 | - This software uses Inductive Automation SDK to interact with the Ignition platform. 13 | -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | # Environment Variables automatically loaded by Docker Compose. 2 | # ref: https://docs.docker.com/compose/reference/envvars/ 3 | COMPOSE_PROJECT_NAME=ignition-devops 4 | COMPOSE_PATH_SEPARATOR=: 5 | #COMPOSE_FILE=docker-compose-automated.yml 6 | COMPOSE_FILE=docker-compose.yml 7 | -------------------------------------------------------------------------------- /docker/docker-compose-automated.yml: -------------------------------------------------------------------------------- 1 | # ICC 2022 - Docker Basics Workshop - Part II, Docker Compose 2 | # ref: https://github.com/compose-spec/compose-spec/blob/master/spec.md 3 | --- 4 | services: 5 | gateway: 6 | build: 7 | context: gw-build 8 | dockerfile: Dockerfile 9 | args: 10 | IGNITION_VERSION: ${IGNITION_VERSION:-8.1.26} 11 | SUPPLEMENTAL_MODULES: "git" 12 | GATEWAY_ADMIN_USERNAME: admin 13 | secrets: 14 | - gateway-admin-password 15 | hostname: gateway 16 | ports: 17 | - 9088:8088 18 | environment: 19 | - ACCEPT_IGNITION_EULA=Y 20 | #- GATEWAY_ADMIN_USERNAME=admin 21 | #- GATEWAY_ADMIN_PASSWORD_FILE=/run/secrets/gateway-admin-password 22 | - GATEWAY_GIT_USER_SECRET_FILE=/run/secrets/gateway-git-user-secret 23 | - IGNITION_EDITION=standard 24 | networks: 25 | - default 26 | volumes: 27 | - gateway_data:/usr/local/bin/ignition/data 28 | - ./gw-init/git.conf:/usr/local/bin/ignition/data/git.conf 29 | secrets: 30 | - gateway-git-user-secret 31 | - gateway-admin-password 32 | command: > 33 | -n Ignition-DevOps 34 | 35 | networks: 36 | default: 37 | 38 | secrets: 39 | gateway-git-user-secret: 40 | file: gw-secrets/GATEWAY_GIT_USER_SECRET 41 | gateway-admin-password: 42 | file: gw-secrets/GATEWAY_ADMIN_PASSWORD 43 | 44 | volumes: 45 | gateway_data: 46 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # ICC 2022 - Docker Basics Workshop - Part II, Docker Compose 2 | # ref: https://github.com/compose-spec/compose-spec/blob/master/spec.md 3 | --- 4 | services: 5 | gateway: 6 | image: inductiveautomation/ignition:8.1.26 7 | hostname: gateway 8 | ports: 9 | - 9088:8088 10 | environment: 11 | - ACCEPT_IGNITION_EULA=Y 12 | - GATEWAY_ADMIN_USERNAME=admin 13 | - GATEWAY_ADMIN_PASSWORD_FILE=/run/secrets/gateway-admin-password 14 | - GATEWAY_GIT_USER_SECRET_FILE=/run/secrets/gateway-git-user-secret 15 | - IGNITION_EDITION=standard 16 | networks: 17 | - default 18 | volumes: 19 | - gateway_data:/usr/local/bin/ignition/data 20 | - ./modules/Git-1.0.2.modl:/usr/local/bin/ignition/user-lib/modules/Git-1.0.2.modl 21 | - ./gw-init/git.conf:/usr/local/bin/ignition/data/git.conf 22 | secrets: 23 | - gateway-git-user-secret 24 | - gateway-admin-password 25 | command: > 26 | -n Ignition-DevOps 27 | 28 | networks: 29 | default: 30 | 31 | secrets: 32 | gateway-git-user-secret: 33 | file: gw-secrets/GATEWAY_GIT_USER_SECRET 34 | gateway-admin-password: 35 | file: gw-secrets/GATEWAY_ADMIN_PASSWORD 36 | 37 | volumes: 38 | gateway_data: 39 | -------------------------------------------------------------------------------- /docker/gw-build/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.5 2 | ARG IGNITION_VERSION=${IGNITION_VERSION} 3 | FROM inductiveautomation/ignition:${IGNITION_VERSION} as prep 4 | 5 | # Temporarily become root for system-level updates (required for 8.1.26+) 6 | USER root 7 | 8 | # Install some prerequisite packages 9 | RUN apt-get update && apt-get install -y wget ca-certificates jq zip unzip sqlite3 10 | 11 | ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.15/Azure-Injector-signed.modl" 12 | ARG SUPPLEMENTAL_AZUREIOTINJECTOR_DOWNLOAD_SHA256="e952629cebaad75825cf6e57c09c01f5f1772065a6e87cc0cda45fdca4b29f13" 13 | ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.15/MQTT-Transmission-signed.modl" 14 | ARG SUPPLEMENTAL_MQTTTRANSMISSION_DOWNLOAD_SHA256="1e1e7428cba02dce6e579e6c82e4b8ad30d892438a7504aa0481eff7a5f87952" 15 | ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Transmission-signed.modl" 16 | ARG SUPPLEMENTAL_MQTTTRANSMISSIONNIGHTLY_DOWNLOAD_SHA256="notused" 17 | ARG SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.15/MQTT-Engine-signed.modl" 18 | ARG SUPPLEMENTAL_MQTTENGINE_DOWNLOAD_SHA256="5693c22b391e6da31351f2bb9245ad65e96dff9d02c0a322cb70eb11a2fb86b6" 19 | ARG SUPPLEMENTAL_MQTTENGINENIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Engine-signed.modl" 20 | ARG SUPPLEMENTAL_MQTTENGINENIGHTLY_DOWNLOAD_SHA256="notused" 21 | ARG SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_URL="https://files.inductiveautomation.com/third-party/cirrus-link/4.0.15/MQTT-Distributor-signed.modl" 22 | ARG SUPPLEMENTAL_MQTTDISTRIBUTOR_DOWNLOAD_SHA256="5c81be13a9f749899825a99a109502c1eb28be940d060301a1ddf9967d488f9e" 23 | ARG SUPPLEMENTAL_MQTTDISTRIBUTORNIGHTLY_DOWNLOAD_URL="https://ignition-modules-nightly.s3.amazonaws.com/Ignition8/MQTT-Distributor-signed.modl" 24 | ARG SUPPLEMENTAL_MQTTDISTRIBUTORNIGHTLY_DOWNLOAD_SHA256="notused" 25 | ARG SUPPLEMENTAL_GIT_DOWNLOAD_URL="https://www.axone-io.com/Files/Modules/GIT/1.0.2/doc/module/Git-1.0.2.modl" 26 | ARG SUPPLEMENTAL_GIT_DOWNLOAD_SHA256="notused" 27 | ARG SUPPLEMENTAL_MODULES 28 | 29 | # Set working directory for this prep image and ensure that exits from sub-shells bubble up and report an error 30 | WORKDIR /root 31 | SHELL [ "/usr/bin/env", "-S", "bash", "-euo", "pipefail", "-O", "inherit_errexit", "-c" ] 32 | 33 | # Retrieve all targeted modules and verify their integrity 34 | COPY --chmod=0755 retrieve-modules.sh . 35 | RUN ./retrieve-modules.sh \ 36 | -m "${SUPPLEMENTAL_MODULES:-}" 37 | 38 | # Set CERTIFICATES/EULAS acceptance in gateway backup config db 39 | COPY base.gwbk . 40 | COPY --chmod=0755 register-module.sh register-password.sh ./ 41 | ARG GATEWAY_ADMIN_USERNAME="admin" 42 | RUN --mount=type=secret,id=gateway-admin-password \ 43 | unzip -q base.gwbk db_backup_sqlite.idb && \ 44 | shopt -s nullglob; \ 45 | for module in *.modl; do \ 46 | ./register-module.sh \ 47 | -f "${module}" \ 48 | -d db_backup_sqlite.idb; \ 49 | done; \ 50 | shopt -u nullglob && \ 51 | ./register-password.sh \ 52 | -u "${GATEWAY_ADMIN_USERNAME}" \ 53 | -f /run/secrets/gateway-admin-password \ 54 | -d db_backup_sqlite.idb && \ 55 | zip -q -f base.gwbk db_backup_sqlite.idb || \ 56 | if [[ ${ZIP_EXIT_CODE:=$?} == 12 ]]; then \ 57 | echo "No changes to internal database needed during module registration."; \ 58 | else \ 59 | echo "Unknown error (${ZIP_EXIT_CODE}) encountered during re-packaging of config db, exiting." && \ 60 | exit ${ZIP_EXIT_CODE}; \ 61 | fi 62 | 63 | # Final Image 64 | FROM inductiveautomation/ignition:${IGNITION_VERSION} as final 65 | 66 | # Temporarily become root for system-level updates (required for 8.1.26+) 67 | USER root 68 | 69 | # Add supplemental packages, such as git if needed/desired 70 | RUN apt-get update && \ 71 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 72 | git && \ 73 | rm -rf /var/lib/apt/lists/* 74 | 75 | # Embed modules and base gwbk from prep image as well as entrypoint shim 76 | COPY --from=prep --chown=ignition:ignition /root/*.modl ${IGNITION_INSTALL_LOCATION}/user-lib/modules/ 77 | COPY --from=prep --chown=ignition:ignition /root/base.gwbk ${IGNITION_INSTALL_LOCATION}/base.gwbk 78 | COPY --chmod=0755 --chown=root:root docker-entrypoint-shim.sh /usr/local/bin/ 79 | 80 | # Return to ignition user 81 | USER ignition 82 | 83 | # Supplement other default environment variables 84 | ENV ACCEPT_IGNITION_EULA=Y \ 85 | IGNITION_EDITION=standard \ 86 | GATEWAY_MODULES_ENABLED=all 87 | 88 | # Target the entrypoint shim for any custom logic prior to gateway launch 89 | ENTRYPOINT [ "docker-entrypoint-shim.sh" ] 90 | -------------------------------------------------------------------------------- /docker/gw-build/base.gwbk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AXONE-IO/ignition-git-module/6e3f3cb4135ba47223439acad23ad780363f02df/docker/gw-build/base.gwbk -------------------------------------------------------------------------------- /docker/gw-build/docker-entrypoint-shim.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ### add other custom logic here that will run at container launch 5 | 6 | # Kick off the built-in entrypoint, with an in-built restore (-r ) directive 7 | exec docker-entrypoint.sh -r base.gwbk "$@" 8 | -------------------------------------------------------------------------------- /docker/gw-build/register-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | ############################################################################### 6 | # Performs auto-acceptance of EULA and import of certificates for third-party modules 7 | ############################################################################### 8 | function main() { 9 | if [ ! -f "${MODULE_LOCATION}" ]; then 10 | echo "" 11 | return 0 # Silently exit if there is no /modules path 12 | elif [ ! -f "${DB_LOCATION}" ]; then 13 | echo "WARNING: ${DB_FILE} not found, skipping module registration" 14 | return 0 15 | fi 16 | 17 | register_module 18 | } 19 | 20 | ############################################################################### 21 | # Register the module with the target Config DB 22 | ############################################################################### 23 | function register_module() { 24 | local SQLITE3=( sqlite3 "${DB_LOCATION}" ) 25 | 26 | # Tie into db 27 | local keytool module_sourcepath 28 | module_basename=$(basename "${MODULE_LOCATION}") 29 | module_sourcepath=${MODULE_LOCATION} 30 | keytool=$(which keytool) 31 | 32 | echo "Processing Module: ${module_basename}" 33 | 34 | # Populate CERTIFICATES table 35 | local cert_info subject_name thumbprint next_certificates_id thumbprint_already_exists 36 | cert_info=$( unzip -qq -c "${module_sourcepath}" certificates.p7b | $keytool -printcert -v | head -n 9 ) 37 | thumbprint=$( echo "${cert_info}" | grep -A 2 "Certificate fingerprints" | grep SHA1 | cut -d : -f 2- | sed -e 's/\://g' | awk '{$1=$1;print tolower($0)}' ) 38 | subject_name=$( echo "${cert_info}" | grep -m 1 -Po '^Owner: CN=\K(.+)(?=, OU)' | sed -e 's/"//g' ) 39 | echo " Thumbprint: ${thumbprint}" 40 | echo " Subject Name: ${subject_name}" 41 | next_certificates_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(CERTIFICATES_ID)+1,1) FROM CERTIFICATES" ) 42 | thumbprint_already_exists=$( "${SQLITE3[@]}" "SELECT 1 FROM CERTIFICATES WHERE lower(hex(THUMBPRINT)) = '${thumbprint}'" ) 43 | if [ "${thumbprint_already_exists}" != "1" ]; then 44 | echo " Accepting Certificate as CERTIFICATES_ID=${next_certificates_id}" 45 | "${SQLITE3[@]}" "INSERT INTO CERTIFICATES (CERTIFICATES_ID, THUMBPRINT, SUBJECTNAME) VALUES (${next_certificates_id}, x'${thumbprint}', '${subject_name}'); UPDATE SEQUENCES SET val=${next_certificates_id} WHERE name='CERTIFICATES_SEQ'" 46 | else 47 | echo " Thumbprint already found in CERTIFICATES table, skipping INSERT" 48 | fi 49 | 50 | # Populate EULAS table 51 | local next_eulas_id license_crc32 module_id module_id_already_exists 52 | next_eulas_id=$( "${SQLITE3[@]}" "SELECT COALESCE(MAX(EULAS_ID)+1,1) FROM EULAS" ) 53 | license_crc32=$( unzip -qq -c "${module_sourcepath}" license.html | gzip -c | tail -c8 | od -t u4 -N 4 -A n | cut -c 2- ) 54 | module_id=$( unzip -qq -c "${module_sourcepath}" module.xml | grep -oP '(?<=).*(?=&2 echo " DEBUG: $*" 71 | fi 72 | } 73 | 74 | ############################################################################### 75 | # Print usage information 76 | ############################################################################### 77 | function usage() { 78 | >&2 echo "Usage: $0 -f -d " 79 | } 80 | 81 | # Argument Processing 82 | while getopts ":hvf:d:" opt; do 83 | case "$opt" in 84 | v) 85 | verbose=1 86 | ;; 87 | f) 88 | MODULE_LOCATION="${OPTARG}" 89 | ;; 90 | d) 91 | DB_LOCATION="${OPTARG}" 92 | DB_FILE=$(basename "${DB_LOCATION}") 93 | ;; 94 | h) 95 | usage 96 | exit 0 97 | ;; 98 | \?) 99 | usage 100 | echo "Invalid option: -${OPTARG}" >&2 101 | exit 1 102 | ;; 103 | :) 104 | usage 105 | echo "Invalid option: -${OPTARG} requires an argument" >&2 106 | exit 1 107 | ;; 108 | esac 109 | done 110 | 111 | # shift positional args based on number consumed by getopts 112 | shift $((OPTIND-1)) 113 | 114 | if [ -z "${MODULE_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then 115 | usage 116 | exit 1 117 | fi 118 | 119 | main -------------------------------------------------------------------------------- /docker/gw-build/register-password.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | ############################################################################### 6 | # Update an Ignition SQLite Configuration DB with a baseline username/password 7 | # ---------------------------------------------------------------------------- 8 | # ref: https://gist.github.com/thirdgen88/c4257bd4c47b6cc7194d1f5e7cbd6444 9 | ############################################################################### 10 | function main() { 11 | if [ ! -f "${SECRET_LOCATION}" ]; then 12 | echo "" 13 | return 0 # Silently exit if there is no secret at target path 14 | elif [ ! -f "${DB_LOCATION}" ]; then 15 | echo "WARNING: ${DB_FILE} not found, skipping password registration" 16 | return 0 17 | fi 18 | 19 | register_password 20 | } 21 | 22 | ############################################################################### 23 | # Updates the target Config DB with the target username and salted pw hash 24 | ############################################################################### 25 | function register_password() { 26 | local SQLITE3=( sqlite3 "${DB_LOCATION}" ) password_hash 27 | 28 | echo "Registering Admin Password with Configuration DB" 29 | 30 | # Generate Salted PW Hash 31 | password_hash=$(generate_salted_hash "$(<"${SECRET_LOCATION}")") 32 | 33 | # Update INTERNALUSERTABLE 34 | echo " Setting default admin user to USERNAME='${GATEWAY_ADMIN_USERNAME}' and PASSWORD='${password_hash}'" 35 | "${SQLITE3[@]}" "UPDATE INTERNALUSERTABLE SET USERNAME='${GATEWAY_ADMIN_USERNAME}', PASSWORD='${password_hash}' WHERE PROFILEID=1 AND USERID=1" 36 | } 37 | 38 | ############################################################################### 39 | # Processes password input and translates to salted hash 40 | ############################################################################### 41 | function generate_salted_hash() { 42 | local -u auth_salt 43 | local auth_pwhash auth_pwsalthash auth_password password_input 44 | password_input="${1}" 45 | 46 | auth_salt=$(date +%s | sha256sum | head -c 8) 47 | debug "auth_salt is ${auth_salt}" 48 | auth_pwhash=$(printf %s "${password_input}" | sha256sum - | cut -c -64) 49 | debug "auth_pwhash is ${auth_pwhash}" 50 | auth_pwsalthash=$(printf %s "${password_input}${auth_salt}" | sha256sum - | cut -c -64) 51 | debug "auth_pwsalthash is ${auth_pwsalthash}" 52 | auth_password="[${auth_salt}]${auth_pwsalthash}" 53 | 54 | echo "${auth_password}" 55 | } 56 | 57 | ############################################################################### 58 | # Outputs to stderr 59 | ############################################################################### 60 | function debug() { 61 | # shellcheck disable=SC2236 62 | if [ ! -z ${verbose+x} ]; then 63 | >&2 echo " DEBUG: $*" 64 | fi 65 | } 66 | 67 | ############################################################################### 68 | # Print usage information 69 | ############################################################################### 70 | function usage() { 71 | >&2 echo "Usage: $0 -u -f -d " 72 | } 73 | 74 | # Argument Processing 75 | while getopts ":hvu:f:d:" opt; do 76 | case "$opt" in 77 | v) 78 | verbose=1 79 | ;; 80 | u) 81 | GATEWAY_ADMIN_USERNAME="${OPTARG}" 82 | ;; 83 | f) 84 | SECRET_LOCATION="${OPTARG}" 85 | ;; 86 | d) 87 | DB_LOCATION="${OPTARG}" 88 | DB_FILE=$(basename "${DB_LOCATION}") 89 | ;; 90 | h) 91 | usage 92 | exit 0 93 | ;; 94 | \?) 95 | usage 96 | echo "Invalid option: -${OPTARG}" >&2 97 | exit 1 98 | ;; 99 | :) 100 | usage 101 | echo "Invalid option: -${OPTARG} requires an argument" >&2 102 | exit 1 103 | ;; 104 | esac 105 | done 106 | 107 | # shift positional args based on number consumed by getopts 108 | shift $((OPTIND-1)) 109 | 110 | if [ -z "${GATEWAY_ADMIN_USERNAME:-}" ] || [ -z "${SECRET_LOCATION:-}" ] || [ -z "${DB_LOCATION:-}" ]; then 111 | usage 112 | exit 1 113 | fi 114 | 115 | main -------------------------------------------------------------------------------- /docker/gw-build/retrieve-modules.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s inherit_errexit 4 | 5 | ############################################################################### 6 | # Retrieves third-party modules and verifies their checksums 7 | ############################################################################### 8 | function main() { 9 | if [ -z "${SUPPLEMENTAL_MODULES}" ]; then 10 | return 0 # Silently exit if there are no supplemental modules to target 11 | fi 12 | 13 | retrieve_modules 14 | } 15 | 16 | ############################################################################### 17 | # Download the modules 18 | ############################################################################### 19 | function retrieve_modules() { 20 | IFS=', ' read -r -a module_install_key_arr <<< "${SUPPLEMENTAL_MODULES}" 21 | for module_install_key in "${module_install_key_arr[@]}"; do 22 | download_url_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_URL" 23 | download_sha256_env="SUPPLEMENTAL_${module_install_key^^}_DOWNLOAD_SHA256" 24 | if [ -n "${!download_url_env:-}" ] && [ -n "${!download_sha256_env:-}" ]; then 25 | download_basename=$(basename "${!download_url_env}") 26 | wget --ca-certificate=/etc/ssl/certs/ca-certificates.crt --referer https://inductiveautomation.com/* "${!download_url_env}" && \ 27 | [[ "notused" == "${!download_sha256_env}" ]] || echo "${!download_sha256_env}" "${download_basename}" | sha256sum -c - 28 | else 29 | echo "Error finding specified module ${module_install_key} in build args, aborting..." 30 | exit 1 31 | fi 32 | done 33 | } 34 | 35 | ############################################################################### 36 | # Outputs to stderr 37 | ############################################################################### 38 | function debug() { 39 | # shellcheck disable=SC2236 40 | if [ ! -z ${verbose+x} ]; then 41 | >&2 echo " DEBUG: $*" 42 | fi 43 | } 44 | 45 | ############################################################################### 46 | # Print usage information 47 | ############################################################################### 48 | function usage() { 49 | >&2 echo "Usage: $0 -m \"space-separated modules list\"" 50 | >&2 echo " -m: space-separated list of module identifiers to download" 51 | } 52 | 53 | # Argument Processing 54 | while getopts ":hvm:" opt; do 55 | case "$opt" in 56 | v) 57 | verbose=1 58 | ;; 59 | m) 60 | SUPPLEMENTAL_MODULES="${OPTARG}" 61 | ;; 62 | h) 63 | usage 64 | exit 0 65 | ;; 66 | \?) 67 | usage 68 | echo "Invalid option: -${OPTARG}" >&2 69 | exit 1 70 | ;; 71 | :) 72 | usage 73 | echo "Invalid option: -${OPTARG} requires an argument" >&2 74 | exit 1 75 | ;; 76 | esac 77 | done 78 | 79 | # shift positional args based on number consumed by getopts 80 | shift $((OPTIND-1)) 81 | 82 | # exit on missing required args 83 | if [ -z "${SUPPLEMENTAL_MODULES:-}" ]; then 84 | usage 85 | exit 1 86 | fi 87 | 88 | main -------------------------------------------------------------------------------- /docker/gw-init/git.conf: -------------------------------------------------------------------------------- 1 | repo.uri= 2 | repo.branch= 3 | ignition.project.name= 4 | 5 | ignition.user.name= 6 | user.name= 7 | user.email= 8 | 9 | commissioning.import.themes=TRUE 10 | commissioning.import.tags=TRUE 11 | commissioning.import.images=TRUE 12 | -------------------------------------------------------------------------------- /docker/gw-secrets/GATEWAY_ADMIN_PASSWORD: -------------------------------------------------------------------------------- 1 | password -------------------------------------------------------------------------------- /docker/gw-secrets/GATEWAY_GIT_USER_SECRET: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AXONE-IO/ignition-git-module/6e3f3cb4135ba47223439acad23ad780363f02df/docker/gw-secrets/GATEWAY_GIT_USER_SECRET -------------------------------------------------------------------------------- /docker/readme.md: -------------------------------------------------------------------------------- 1 | # Docker compose guide 2 | 3 | ### Prerequisites : 4 | - Docker : https://www.docker.com/ 5 | - Configure .env file (especially with the right COMPOSE_FILE) 6 | 7 | ### Classic Docker Compose : 8 | - Put git module in ./modules/ folder 9 | - Fill ./gw-ini/git.conf with right configurations 10 | - Fill the ./gw-secrets/GATEWAY_ADMIN_PASSWORD file with right password 11 | - Fill the ./gw-secrets/GATEWAY_GIT_USER_SECRET file with the right password or ssh key relative to git.conf (not necessary if the password is directly filled in git.conf, but less secure) 12 | - Modify the docker-compose to your liking 13 | - Run the command line "docker compose up" 14 | 15 | ### Classic Docker Compose full automated (Derived Image Solution) : 16 | Based on : https://github.com/thirdgen88/ignition-derived-example 17 | - Fill the right version of git module in ./gw-build/Dockerfile 18 | - SUPPLEMENTAL_GIT_DOWNLOAD_URL 19 | - Fill ./gw-ini/git.conf with right configurations 20 | - Fill the ./gw-secrets/GATEWAY_ADMIN_PASSWORD file with right password 21 | - Fill the ./gw-secrets/GATEWAY_GIT_USER_SECRET file with the right password or ssh key relative to git.conf (not necessary if the password is directly filled in git.conf, but less secure) 22 | - Modify the docker-compose to your liking 23 | - Run the command line "docker compose up" 24 | -------------------------------------------------------------------------------- /git-build/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Git Module License 7 | 8 | 9 |

THE BEERWARE LICENSE (Revision 42):

10 |
11 | 12 |

13 | Enzo Sagnelonge, working for AXONE-IO, wrote this code.
14 | As long as you retain this notice, you can do whatever you want with this stuff.
15 | If we meet someday, and you think this stuff is worth it, you can buy me a beer in return. 16 |

17 |
18 | 19 |
20 | 21 |

Third-Party Licenses :

22 |
    23 |
  • 24 | This software uses the JGit library, which is distributed under the Eclipse Distribution License (New BSD License). 25 |
  • 26 |
  • 27 | This software uses the Intellij forms_rt library, which is distributed under the Apache License 2. 28 |
  • 29 |
  • 30 | This software uses Inductive Automation SDK to interact with the Ignition platform. 31 |
  • 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /git-build/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | git 8 | com.axone_io.ignition 9 | 1.0.2 10 | 11 | 12 | git-build 13 | 14 | 15 | Europe/Paris 16 | 17 | UTC+2 18 | 19 | fr_FR 20 | yyyyMMddHH 21 | 22 | 23 | 24 | 25 | com.axone_io.ignition 26 | git-client 27 | ${project.version} 28 | 29 | 30 | com.axone_io.ignition 31 | git-common 32 | ${project.version} 33 | 34 | 35 | com.axone_io.ignition 36 | git-designer 37 | ${project.version} 38 | 39 | 40 | com.axone_io.ignition 41 | git-gateway 42 | ${project.version} 43 | 44 | 45 | 46 | 47 | 48 | 49 | com.inductiveautomation.ignitionsdk 50 | ignition-maven-plugin 51 | 1.1.0 52 | 53 | 54 | 55 | package-modl 56 | package 57 | 58 | modl 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | git-client 67 | C 68 | 69 | 70 | git-common 71 | CDG 72 | 73 | 74 | git-designer 75 | CD 76 | 77 | 78 | git-gateway 79 | G 80 | 81 | 82 | 83 | com.axone_io.ignition.git 84 | ${module-name} 85 | ${module-description} 86 | ${project.parent.version}.${maven.build.timestamp} 87 | ${ignition-platform-version} 88 | license.html 89 | 90 | 91 | 92 | C 93 | com.axone_io.ignition.git.ClientHook 94 | 95 | 96 | D 97 | com.axone_io.ignition.git.DesignerHook 98 | 99 | 100 | G 101 | com.axone_io.ignition.git.GatewayHook 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /git-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | git 8 | com.axone_io.ignition 9 | 1.0.2 10 | 11 | 12 | git-client 13 | 14 | 15 | 16 | com.axone_io.ignition 17 | git-common 18 | ${project.version} 19 | 20 | 21 | com.inductiveautomation.ignitionsdk 22 | ignition-common 23 | ${ignition-sdk-version} 24 | pom 25 | provided 26 | 27 | 28 | com.inductiveautomation.ignitionsdk 29 | client-api 30 | ${ignition-sdk-version} 31 | pom 32 | provided 33 | 34 | 35 | com.inductiveautomation.ignitionsdk 36 | designer-api 37 | ${ignition-sdk-version} 38 | pom 39 | provided 40 | 41 | 42 | com.inductiveautomation.ignitionsdk 43 | vision-designer-api 44 | ${ignition-sdk-version} 45 | pom 46 | provided 47 | 48 | 49 | com.inductiveautomation.ignitionsdk 50 | vision-client-api 51 | ${ignition-sdk-version} 52 | pom 53 | provided 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-compiler-plugin 62 | 3.2 63 | 64 | ${ignition-java-version} 65 | ${ignition-java-version} 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /git-client/src/main/java/com/axone_io/ignition/git/ClientHook.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.inductiveautomation.ignition.common.script.ScriptManager; 4 | import com.inductiveautomation.ignition.common.script.hints.PropertiesFileDocProvider; 5 | import com.inductiveautomation.vision.api.client.AbstractClientModuleHook; 6 | 7 | public class ClientHook extends AbstractClientModuleHook { 8 | 9 | @Override 10 | public void initializeScriptManager(ScriptManager manager) { 11 | super.initializeScriptManager(manager); 12 | 13 | /*manager.addScriptModule( 14 | "system.git", 15 | new ClientScriptModule(), 16 | new PropertiesFileDocProvider() 17 | );*/ 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /git-client/src/main/java/com/axone_io/ignition/git/ClientScriptModule.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory; 4 | import com.inductiveautomation.ignition.common.Dataset; 5 | 6 | import java.util.List; 7 | 8 | public class ClientScriptModule extends AbstractScriptModule { 9 | 10 | private final GitScriptInterface rpc; 11 | 12 | public ClientScriptModule() { 13 | rpc = ModuleRPCFactory.create( 14 | "com.axone_io.ignition.git", 15 | GitScriptInterface.class 16 | ); 17 | } 18 | 19 | @Override 20 | protected boolean pullImpl(String projectName, String userName) throws Exception { 21 | return rpc.pull(projectName, userName); 22 | } 23 | 24 | @Override 25 | protected boolean pushImpl(String projectName, String userName) throws Exception { 26 | return rpc.push(projectName, userName); 27 | } 28 | 29 | @Override 30 | protected boolean commitImpl(String projectName, String userName, List changes, String message) { 31 | return rpc.commit(projectName, userName, changes, message); 32 | } 33 | 34 | @Override 35 | protected Dataset getUncommitedChangesImpl(String projectName, String userName) { 36 | return rpc.getUncommitedChanges(projectName, userName); 37 | } 38 | 39 | @Override 40 | protected boolean isRegisteredUserImpl(String projectName, String userName){ 41 | return rpc.isRegisteredUser(projectName, userName); 42 | } 43 | 44 | @Override 45 | protected boolean exportConfigImpl(String projectName) { 46 | return rpc.exportConfig(projectName); 47 | } 48 | 49 | @Override 50 | protected void setupLocalRepoImpl(String projectName, String userName) throws Exception { 51 | rpc.setupLocalRepo(projectName, userName); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /git-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | git 8 | com.axone_io.ignition 9 | 1.0.2 10 | 11 | 12 | git-common 13 | 14 | 15 | 16 | com.inductiveautomation.ignitionsdk 17 | ignition-common 18 | ${ignition-sdk-version} 19 | pom 20 | provided 21 | 22 | 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-compiler-plugin 29 | 3.2 30 | 31 | ${ignition-java-version} 32 | ${ignition-java-version} 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /git-common/src/main/java/com/axone_io/ignition/git/AbstractScriptModule.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.inductiveautomation.ignition.common.BundleUtil; 4 | import com.inductiveautomation.ignition.common.Dataset; 5 | import com.inductiveautomation.ignition.common.script.hints.ScriptArg; 6 | import com.inductiveautomation.ignition.common.script.hints.ScriptFunction; 7 | 8 | import java.util.List; 9 | 10 | public abstract class AbstractScriptModule implements GitScriptInterface { 11 | 12 | static { 13 | BundleUtil.get().addBundle( 14 | AbstractScriptModule.class.getSimpleName(), 15 | AbstractScriptModule.class.getClassLoader(), 16 | AbstractScriptModule.class.getName().replace('.', '/') 17 | ); 18 | } 19 | @Override 20 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 21 | public boolean pull(@ScriptArg("projectName") String projectName, 22 | @ScriptArg("userName") String userName) throws Exception { 23 | return pullImpl(projectName, userName); 24 | } 25 | 26 | 27 | @Override 28 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 29 | public boolean push(@ScriptArg("projectName") String projectName, 30 | @ScriptArg("userName")String userName) throws Exception { 31 | return pushImpl(projectName,userName); 32 | } 33 | 34 | @Override 35 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 36 | public boolean commit(@ScriptArg("projectName") String projectName, 37 | @ScriptArg("userName") String userName, 38 | @ScriptArg("changes") List changes, 39 | @ScriptArg("message") String message) { 40 | return commitImpl(projectName, userName, changes, message); 41 | } 42 | 43 | @Override 44 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 45 | public Dataset getUncommitedChanges(@ScriptArg("projectName") String projectName, 46 | @ScriptArg("userName") String userName) { 47 | return getUncommitedChangesImpl(projectName, userName); 48 | } 49 | 50 | @Override 51 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 52 | public boolean isRegisteredUser(@ScriptArg("projectName") String projectName, 53 | @ScriptArg("userName") String userName) { 54 | return isRegisteredUserImpl(projectName, userName); 55 | } 56 | 57 | @Override 58 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 59 | public boolean exportConfig(@ScriptArg("projectName") String projectName) { 60 | return exportConfigImpl(projectName); 61 | } 62 | 63 | @Override 64 | @ScriptFunction(docBundlePrefix = "AbstractScriptModule") 65 | public void setupLocalRepo(@ScriptArg("projectName") String projectName, 66 | @ScriptArg("userName") String userName) throws Exception { 67 | setupLocalRepoImpl(projectName, userName); 68 | } 69 | 70 | protected abstract boolean pullImpl(String projectName, String userName) throws Exception; 71 | protected abstract boolean pushImpl(String projectName, String userName) throws Exception; 72 | protected abstract boolean commitImpl(String projectName, String userName, List changes, String message); 73 | protected abstract Dataset getUncommitedChangesImpl(String projectName, String userName); 74 | protected abstract boolean isRegisteredUserImpl(String projectName, String userName); 75 | protected abstract boolean exportConfigImpl(String projectName); 76 | protected abstract void setupLocalRepoImpl(String projectName, String userName) throws Exception; 77 | 78 | } 79 | -------------------------------------------------------------------------------- /git-common/src/main/java/com/axone_io/ignition/git/GitScriptInterface.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.inductiveautomation.ignition.common.Dataset; 4 | 5 | import java.util.List; 6 | 7 | public interface GitScriptInterface { 8 | 9 | boolean pull(String projectName, String userName) throws Exception; 10 | boolean push(String projectName, String userName) throws Exception; 11 | boolean commit(String projectName, String userName, List changes, String message); 12 | Dataset getUncommitedChanges(String projectName, String userName); 13 | boolean isRegisteredUser(String projectName, String userName); 14 | boolean exportConfig(String projectName); 15 | void setupLocalRepo(String projectName, String userName) throws Exception; 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /git-common/src/main/resources/com/axone_io/ignition/git/AbstractScriptModule.properties: -------------------------------------------------------------------------------- 1 | multiply.desc=Multiple two integers and return the result. 2 | multiply.param.arg0=The first operand. 3 | multiply.param.arg1=The second operand. 4 | multiply.returns=Returns the first operand multiplied by the second. 5 | -------------------------------------------------------------------------------- /git-designer/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | git 8 | com.axone_io.ignition 9 | 1.0.2 10 | 11 | 12 | git-designer 13 | 14 | 15 | 16 | com.axone_io.ignition 17 | git-client 18 | ${project.version} 19 | 20 | 21 | com.axone_io.ignition 22 | git-common 23 | ${project.version} 24 | 25 | 26 | com.intellij 27 | forms_rt 28 | 7.0.3 29 | 30 | 31 | com.inductiveautomation.ignitionsdk 32 | ignition-common 33 | ${ignition-sdk-version} 34 | pom 35 | provided 36 | 37 | 38 | com.inductiveautomation.ignitionsdk 39 | client-api 40 | ${ignition-sdk-version} 41 | pom 42 | provided 43 | 44 | 45 | com.inductiveautomation.ignitionsdk 46 | designer-api 47 | ${ignition-sdk-version} 48 | pom 49 | provided 50 | 51 | 52 | com.inductiveautomation.ignitionsdk 53 | vision-designer-api 54 | ${ignition-sdk-version} 55 | pom 56 | provided 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-compiler-plugin 65 | 3.2 66 | 67 | ${ignition-java-version} 68 | ${ignition-java-version} 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/CommitPopup.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/CommitPopup.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | 4 | import com.axone_io.ignition.git.components.SelectAllHeader; 5 | import com.inductiveautomation.ignition.designer.gui.CommonUI; 6 | import com.intellij.uiDesigner.core.GridConstraints; 7 | import com.intellij.uiDesigner.core.GridLayoutManager; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.imageio.ImageIO; 12 | import javax.swing.*; 13 | import javax.swing.border.TitledBorder; 14 | import javax.swing.plaf.FontUIResource; 15 | import javax.swing.table.DefaultTableModel; 16 | import javax.swing.table.TableColumn; 17 | import javax.swing.text.StyleContext; 18 | import java.awt.*; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Locale; 24 | 25 | public class CommitPopup extends JFrame { 26 | private final Logger logger = LoggerFactory.getLogger(getClass()); 27 | private JPanel panel; 28 | private JTextArea messageTextArea; 29 | private JLabel messageLabel; 30 | private JButton commitBtn; 31 | private JButton cancelBtn; 32 | private JLabel changesLabel; 33 | private JTable changesTable; 34 | 35 | public CommitPopup(Object[][] data, Component parent) { 36 | try { 37 | InputStream commitIconStream = getClass().getResourceAsStream("/com/axone_io/ignition/git/icons/ic_commit.svg"); 38 | ImageIcon commitIcon = new ImageIcon(ImageIO.read(commitIconStream)); 39 | setIconImage(commitIcon.getImage()); 40 | } catch (IOException e) { 41 | logger.trace(e.toString(), e); 42 | } 43 | setContentPane(panel); 44 | setTitle("Commit"); 45 | setSize(500, 500); 46 | setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 47 | setVisible(true); 48 | 49 | changesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); 50 | changesTable.getTableHeader().setReorderingAllowed(false); 51 | 52 | setData(data); 53 | 54 | commitBtn.addActionListener(e -> { 55 | List changes = new ArrayList<>(); 56 | for (int i = 0; i < changesTable.getModel().getRowCount(); i++) { 57 | if ((Boolean) changesTable.getValueAt(i, 0)) { 58 | changes.add((String) changesTable.getValueAt(i, 1)); 59 | } 60 | } 61 | 62 | onActionPerformed(changes, messageTextArea.getText()); 63 | this.dispose(); 64 | }); 65 | 66 | cancelBtn.addActionListener(e -> this.dispose()); 67 | 68 | pack(); 69 | CommonUI.centerComponent(this, parent); 70 | toFront(); 71 | } 72 | 73 | public void resetMessage() { 74 | messageTextArea.setText(""); 75 | } 76 | 77 | public void setData(Object[][] data) { 78 | String[] columnNames = {"", "Resource Name", "Type", "Author"}; 79 | DefaultTableModel model = new DefaultTableModel(data, columnNames) { 80 | public Class getColumnClass(int column) { 81 | switch (column) { 82 | case 0: 83 | return Boolean.class; 84 | case 1: 85 | case 2: 86 | case 3: 87 | default: 88 | return String.class; 89 | } 90 | } 91 | }; 92 | 93 | changesTable.setModel(model); 94 | changesTable.getColumn("").setPreferredWidth(20); 95 | changesTable.getColumn("Resource Name").setPreferredWidth(330); 96 | changesTable.getColumn("Type").setPreferredWidth(100); 97 | changesTable.getColumn("Author").setPreferredWidth(100); 98 | 99 | TableColumn tc = changesTable.getColumnModel().getColumn(0); 100 | tc.setHeaderRenderer(new SelectAllHeader(changesTable, 0)); 101 | } 102 | 103 | 104 | public void onActionPerformed(List changes, String message) { 105 | 106 | } 107 | 108 | { 109 | // GUI initializer generated by IntelliJ IDEA GUI Designer 110 | // >>> IMPORTANT!! <<< 111 | // DO NOT EDIT OR ADD ANY CODE HERE! 112 | $$$setupUI$$$(); 113 | } 114 | 115 | /** 116 | * Method generated by IntelliJ IDEA GUI Designer 117 | * >>> IMPORTANT!! <<< 118 | * DO NOT edit this method OR call it in your code! 119 | * 120 | * @noinspection ALL 121 | */ 122 | private void $$$setupUI$$$() { 123 | panel = new JPanel(); 124 | panel.setLayout(new GridLayoutManager(5, 2, new Insets(5, 5, 5, 5), -1, -1)); 125 | panel.setPreferredSize(new Dimension(500, 500)); 126 | messageLabel = new JLabel(); 127 | Font messageLabelFont = this.$$$getFont$$$(null, Font.BOLD, -1, messageLabel.getFont()); 128 | if (messageLabelFont != null) messageLabel.setFont(messageLabelFont); 129 | messageLabel.setText("Your commit message :"); 130 | panel.add(messageLabel, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 131 | commitBtn = new JButton(); 132 | commitBtn.setBackground(new Color(-11555609)); 133 | commitBtn.setForeground(new Color(-1)); 134 | commitBtn.setText("Commit"); 135 | panel.add(commitBtn, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 136 | cancelBtn = new JButton(); 137 | cancelBtn.setText("Cancel"); 138 | panel.add(cancelBtn, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 139 | changesLabel = new JLabel(); 140 | Font changesLabelFont = this.$$$getFont$$$(null, Font.BOLD, -1, changesLabel.getFont()); 141 | if (changesLabelFont != null) changesLabel.setFont(changesLabelFont); 142 | changesLabel.setText("Changes :"); 143 | panel.add(changesLabel, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 144 | final JScrollPane scrollPane1 = new JScrollPane(); 145 | panel.add(scrollPane1, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); 146 | changesTable = new JTable(); 147 | scrollPane1.setViewportView(changesTable); 148 | final JPanel panel1 = new JPanel(); 149 | panel1.setLayout(new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1)); 150 | panel.add(panel1, new GridConstraints(3, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 151 | panel1.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(new Color(-4143670)), null, TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, null)); 152 | messageTextArea = new JTextArea(); 153 | messageTextArea.setText(""); 154 | panel1.add(messageTextArea, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_WANT_GROW, null, new Dimension(150, 50), null, 0, false)); 155 | } 156 | 157 | /** 158 | * @noinspection ALL 159 | */ 160 | private Font $$$getFont$$$(String fontName, int style, int size, Font currentFont) { 161 | if (currentFont == null) return null; 162 | String resultName; 163 | if (fontName == null) { 164 | resultName = currentFont.getName(); 165 | } else { 166 | Font testFont = new Font(fontName, Font.PLAIN, 10); 167 | if (testFont.canDisplay('a') && testFont.canDisplay('1')) { 168 | resultName = fontName; 169 | } else { 170 | resultName = currentFont.getName(); 171 | } 172 | } 173 | Font font = new Font(resultName, style >= 0 ? style : currentFont.getStyle(), size >= 0 ? size : currentFont.getSize()); 174 | boolean isMac = System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("mac"); 175 | Font fontWithFallback = isMac ? new Font(font.getFamily(), font.getStyle(), font.getSize()) : new StyleContext().getFont(font.getFamily(), font.getStyle(), font.getSize()); 176 | return fontWithFallback instanceof FontUIResource ? fontWithFallback : new FontUIResource(fontWithFallback); 177 | } 178 | 179 | /** 180 | * @noinspection ALL 181 | */ 182 | public JComponent $$$getRootComponent$$$() { 183 | return panel; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/DesignerHook.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.axone_io.ignition.git.actions.GitBaseAction; 4 | import com.axone_io.ignition.git.utils.IconUtils; 5 | import com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory; 6 | import com.inductiveautomation.ignition.common.BundleUtil; 7 | import com.inductiveautomation.ignition.common.SessionInfo; 8 | import com.inductiveautomation.ignition.common.licensing.LicenseState; 9 | import com.inductiveautomation.ignition.common.project.ChangeOperation; 10 | import com.inductiveautomation.ignition.designer.gui.DesignerToolbar; 11 | import com.inductiveautomation.ignition.designer.gui.StatusBar; 12 | import com.inductiveautomation.ignition.designer.model.DesignerContext; 13 | import com.inductiveautomation.ignition.common.script.ScriptManager; 14 | import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHook; 15 | import com.inductiveautomation.ignition.designer.model.SaveContext; 16 | import com.jidesoft.action.DockableBarManager; 17 | 18 | import javax.swing.*; 19 | import javax.swing.Timer; 20 | import java.util.*; 21 | import java.util.List; 22 | 23 | import static com.axone_io.ignition.git.managers.GitActionManager.showCommitPopup; 24 | 25 | public class DesignerHook extends AbstractDesignerModuleHook { 26 | 27 | public static GitScriptInterface rpc = ModuleRPCFactory.create( 28 | "com.axone_io.ignition.git", 29 | GitScriptInterface.class 30 | ); 31 | public static List changes = new ArrayList<>(); 32 | public static DesignerContext context; 33 | public static String projectName; 34 | public static String userName; 35 | JPanel gitStatusBar; 36 | Timer gitUserTimer; 37 | @Override 38 | public void initializeScriptManager(ScriptManager manager) { 39 | super.initializeScriptManager(manager); 40 | 41 | /*manager.addScriptModule( 42 | "system.git", 43 | new ClientScriptModule(), 44 | new PropertiesFileDocProvider() 45 | );*/ 46 | } 47 | 48 | @Override 49 | public void startup(DesignerContext context, LicenseState activationState) throws Exception { 50 | super.startup(context, activationState); 51 | DesignerHook.context = context; 52 | BundleUtil.get().addBundle("DesignerHook", getClass(), "DesignerHook"); 53 | 54 | projectName = context.getProjectName(); 55 | 56 | Optional sessionInfo = context.getResourceEditManager().getCurrentSessionInfo(); 57 | userName = sessionInfo.isPresent() ? sessionInfo.get().getUsername() : ""; 58 | 59 | rpc.setupLocalRepo(projectName, userName); 60 | 61 | initStatusBar(); 62 | initToolBar(); 63 | 64 | } 65 | 66 | private void initStatusBar(){ 67 | StatusBar statusBar = context.getStatusBar(); 68 | gitStatusBar = new JPanel(); 69 | 70 | JLabel gitIconLabel = new JLabel(IconUtils.getIcon("/com/axone_io/ignition/git/icons/ic_git.svg")); 71 | gitIconLabel.setSize(35, 35); 72 | gitStatusBar.add(gitIconLabel); 73 | 74 | gitStatusBar.add(new JLabel(userName)); 75 | 76 | boolean userValid = rpc.isRegisteredUser(projectName, userName); 77 | String userIconPath = userValid ? "/com/axone_io/ignition/git/icons/ic_verified_user.svg" : "/com/axone_io/ignition/git/icons/ic_unregister_user.svg"; 78 | JLabel labelUserIcon = new JLabel(IconUtils.getIcon(userIconPath)); 79 | labelUserIcon.setSize(35,35); 80 | gitStatusBar.add(labelUserIcon); 81 | 82 | statusBar.addDisplay(gitStatusBar); 83 | 84 | gitUserTimer = new Timer(10000, e -> { 85 | boolean valid = rpc.isRegisteredUser(projectName, userName); 86 | String userIconPath1 = valid ? "/com/axone_io/ignition/git/icons/ic_verified_user.svg" : "/com/axone_io/ignition/git/icons/ic_unregister_user.svg"; 87 | labelUserIcon.setIcon(IconUtils.getIcon(userIconPath1)); 88 | }); 89 | 90 | gitUserTimer.start(); 91 | } 92 | 93 | private void initToolBar() { 94 | DockableBarManager toolBarManager = context.getToolbarManager(); 95 | DesignerToolbar toolbar = new DesignerToolbar("Git", "DesignerHook.Toolbar.Name"); 96 | toolbar.add(new GitBaseAction(GitBaseAction.GitActionType.PUSH)); 97 | toolbar.add(new GitBaseAction(GitBaseAction.GitActionType.PULL)); 98 | toolbar.add(new GitBaseAction(GitBaseAction.GitActionType.COMMIT)); 99 | toolbar.add(new GitBaseAction(GitBaseAction.GitActionType.EXPORT)); 100 | 101 | toolBarManager.addDockableBar(toolbar); 102 | } 103 | 104 | @Override 105 | public void notifyProjectSaveStart(SaveContext save) { 106 | changes = context.getProject().getChanges(); 107 | super.notifyProjectSaveStart(save); 108 | } 109 | 110 | @Override 111 | public void notifyProjectSaveDone(){ 112 | super.notifyProjectSaveDone(); 113 | 114 | showCommitPopup(projectName, userName); 115 | } 116 | 117 | @Override 118 | public void shutdown() { 119 | super.shutdown(); 120 | 121 | DockableBarManager toolBarManager = context.getToolbarManager(); 122 | toolBarManager.removeDockableBar("Git"); 123 | 124 | StatusBar statusBar = context.getStatusBar(); 125 | statusBar.removeDisplay(gitStatusBar); 126 | 127 | gitUserTimer.stop(); 128 | } 129 | 130 | 131 | } 132 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/actions/GitBaseAction.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.actions; 2 | 3 | import com.axone_io.ignition.git.utils.IconUtils; 4 | import com.inductiveautomation.ignition.client.util.action.BaseAction; 5 | import com.inductiveautomation.ignition.client.util.gui.ErrorUtil; 6 | import com.inductiveautomation.ignition.common.BundleUtil; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.ActionEvent; 12 | import java.util.List; 13 | 14 | import static com.axone_io.ignition.git.DesignerHook.*; 15 | import static com.axone_io.ignition.git.managers.GitActionManager.showCommitPopup; 16 | import static com.axone_io.ignition.git.managers.GitActionManager.showConfirmPopup; 17 | 18 | public class GitBaseAction extends BaseAction { 19 | private static final Logger logger = LoggerFactory.getLogger(GitBaseAction.class); 20 | 21 | public enum GitActionType { 22 | PULL( 23 | "DesignerHook.Actions.Pull", 24 | "/com/axone_io/ignition/git/icons/ic_pull.svg" 25 | ), 26 | PUSH( 27 | "DesignerHook.Actions.Push", 28 | "/com/axone_io/ignition/git/icons/ic_push.svg" 29 | ), 30 | COMMIT( 31 | "DesignerHook.Actions.Commit", 32 | "/com/axone_io/ignition/git/icons/ic_commit.svg" 33 | ), 34 | EXPORT( 35 | "DesignerHook.Actions.ExportGatewayConfig", 36 | "/com/axone_io/ignition/git/icons/ic_folder.svg" 37 | ); 38 | 39 | private final String baseBundleKey; 40 | private final String resourcePath; 41 | 42 | GitActionType(String baseBundleKey, String resourcePath) { 43 | this.baseBundleKey = baseBundleKey; 44 | this.resourcePath = resourcePath; 45 | } 46 | 47 | public Icon getIcon() { 48 | return IconUtils.getIcon(resourcePath); 49 | } 50 | } 51 | 52 | GitActionType type; 53 | 54 | public GitBaseAction(GitActionType type) { 55 | super(type.baseBundleKey, type.getIcon()); 56 | this.type = type; 57 | } 58 | 59 | @Override 60 | public void actionPerformed(ActionEvent e) { 61 | handleAction(type); 62 | } 63 | 64 | // Todo : Find a way to refactor with handleAction 65 | public static void handleCommitAction(List changes, String commitMessage) { 66 | String message = BundleUtil.get().getStringLenient(GitActionType.COMMIT.baseBundleKey + ".ConfirmMessage"); 67 | int messageType = JOptionPane.INFORMATION_MESSAGE; 68 | 69 | try { 70 | rpc.commit(projectName, userName, changes, commitMessage); 71 | SwingUtilities.invokeLater(new Thread(() -> showConfirmPopup(message, messageType))); 72 | } catch (Exception ex) { 73 | ErrorUtil.showError(ex); 74 | } 75 | } 76 | 77 | public static void handleAction(GitActionType type) { 78 | String message = BundleUtil.get().getStringLenient(type.baseBundleKey + ".ConfirmMessage"); 79 | int messageType = JOptionPane.INFORMATION_MESSAGE; 80 | boolean confirmPopup = Boolean.TRUE; 81 | 82 | try { 83 | switch (type) { 84 | case PULL: 85 | rpc.pull(projectName, userName); 86 | break; 87 | case PUSH: 88 | rpc.push(projectName, userName); 89 | break; 90 | case COMMIT: 91 | confirmPopup = Boolean.FALSE; 92 | showCommitPopup(projectName, userName); 93 | break; 94 | case EXPORT: 95 | rpc.exportConfig(projectName); 96 | break; 97 | } 98 | if(confirmPopup) SwingUtilities.invokeLater(new Thread(() -> showConfirmPopup(message, messageType))); 99 | 100 | } catch (Exception ex) { 101 | ErrorUtil.showError(ex); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/components/SelectAllHeader.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.components; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.swing.*; 7 | import javax.swing.event.TableModelEvent; 8 | import javax.swing.event.TableModelListener; 9 | import javax.swing.table.*; 10 | import java.awt.*; 11 | import java.awt.event.*; 12 | 13 | public class SelectAllHeader extends JCheckBox implements TableCellRenderer { 14 | private JTable table; 15 | private TableModel tableModel; 16 | private JTableHeader header; 17 | private TableColumnModel tcm; 18 | private int targetColumn; 19 | private int viewColumn; 20 | 21 | private final Logger logger = LoggerFactory.getLogger(getClass()); 22 | public SelectAllHeader(JTable table, int targetColumn) { 23 | super(); 24 | this.table = table; 25 | this.tableModel = table.getModel(); 26 | if (tableModel.getColumnClass(targetColumn) != Boolean.class) { 27 | throw new IllegalArgumentException("Boolean column required."); 28 | } 29 | this.targetColumn = targetColumn; 30 | this.header = table.getTableHeader(); 31 | setHorizontalAlignment(JCheckBox.CENTER); 32 | this.tcm = table.getColumnModel(); 33 | this.applyUI(); 34 | this.addItemListener(new ItemHandler()); 35 | header.addMouseListener(new MouseHandler()); 36 | setBorderPainted(true); 37 | setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1,new Color(-4143670))); 38 | tableModel.addTableModelListener(new ModelHandler()); 39 | 40 | refreshState(); 41 | } 42 | 43 | @Override 44 | public Component getTableCellRendererComponent( 45 | JTable table, Object value, boolean isSelected, 46 | boolean hasFocus, int row, int column) { 47 | return this; 48 | } 49 | 50 | private class ItemHandler implements ItemListener { 51 | 52 | @Override 53 | public void itemStateChanged(ItemEvent e) { 54 | boolean state = e.getStateChange() == ItemEvent.SELECTED; 55 | setSelected(state); 56 | for (int r = 0; r < table.getRowCount(); r++) { 57 | table.setValueAt(state, r, viewColumn); 58 | } 59 | } 60 | } 61 | 62 | @Override 63 | public void updateUI() { 64 | super.updateUI(); 65 | applyUI(); 66 | } 67 | 68 | private void applyUI() { 69 | this.setFont(UIManager.getFont("TableHeader.font")); 70 | this.setBorder(UIManager.getBorder("TableHeader.cellBorder")); 71 | this.setBackground(UIManager.getColor("TableHeader.background")); 72 | this.setForeground(UIManager.getColor("TableHeader.foreground")); 73 | } 74 | 75 | private class MouseHandler extends MouseAdapter { 76 | 77 | @Override 78 | public void mouseClicked(MouseEvent e) { 79 | viewColumn = header.columnAtPoint(e.getPoint()); 80 | int modelColumn = tcm.getColumn(viewColumn).getModelIndex(); 81 | if (modelColumn == targetColumn) { 82 | doClick(); 83 | header.repaint(); 84 | } 85 | } 86 | } 87 | 88 | private class ModelHandler implements TableModelListener { 89 | 90 | @Override 91 | public void tableChanged(TableModelEvent e) { 92 | refreshState(); 93 | } 94 | } 95 | 96 | // Return true if this toggle needs to match the model. 97 | private boolean needsToggle() { 98 | boolean allTrue = true; 99 | boolean allFalse = true; 100 | for (int r = 0; r < tableModel.getRowCount(); r++) { 101 | boolean b = (Boolean) tableModel.getValueAt(r, targetColumn); 102 | allTrue &= b; 103 | allFalse &= !b; 104 | } 105 | return allTrue && !isSelected() || allFalse && isSelected(); 106 | } 107 | 108 | private void refreshState(){ 109 | if (needsToggle()) { 110 | doClick(); 111 | header.repaint(); 112 | } 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/managers/GitActionManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.axone_io.ignition.git.CommitPopup; 4 | import com.axone_io.ignition.git.DesignerHook; 5 | import com.inductiveautomation.ignition.common.Dataset; 6 | import com.inductiveautomation.ignition.common.project.ChangeOperation; 7 | import com.inductiveautomation.ignition.common.project.resource.ProjectResourceId; 8 | 9 | import javax.swing.*; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import static com.axone_io.ignition.git.DesignerHook.context; 14 | import static com.axone_io.ignition.git.DesignerHook.rpc; 15 | import static com.axone_io.ignition.git.actions.GitBaseAction.handleCommitAction; 16 | 17 | public class GitActionManager { 18 | static CommitPopup commitPopup; 19 | 20 | public static Object[][] getCommitPopupData(String projectName, String userName) { 21 | List changes = DesignerHook.changes; 22 | 23 | Dataset ds = rpc.getUncommitedChanges(projectName, userName); 24 | Object[][] data = new Object[ds.getRowCount()][]; 25 | 26 | List resourcesChangedId = new ArrayList<>(); 27 | for (ChangeOperation c : changes) { 28 | ProjectResourceId pri = ChangeOperation.getResourceIdFromChange(c); 29 | resourcesChangedId.add(pri.getResourcePath().toString()); 30 | } 31 | 32 | for (int i = 0; i < ds.getRowCount(); i++) { 33 | String resource = (String) ds.getValueAt(i, "resource"); 34 | 35 | boolean toAdd = resourcesChangedId.contains(resource); 36 | Object[] row = {toAdd, resource, ds.getValueAt(i, "type"), ds.getValueAt(i, "actor")}; 37 | data[i] = row; 38 | } 39 | 40 | return data; 41 | } 42 | 43 | public static void showCommitPopup(String projectName, String userName) { 44 | Object[][] data = GitActionManager.getCommitPopupData(projectName, userName); 45 | if (commitPopup != null) { 46 | commitPopup.setData(data); 47 | commitPopup.setVisible(true); 48 | commitPopup.toFront(); 49 | } else { 50 | commitPopup = new CommitPopup(data, context.getFrame()) { 51 | @Override 52 | public void onActionPerformed(List changes, String commitMessage) { 53 | handleCommitAction(changes, commitMessage); 54 | resetMessage(); 55 | } 56 | }; 57 | } 58 | } 59 | 60 | public static void showConfirmPopup(String message, int messageType) { 61 | JOptionPane.showConfirmDialog(context.getFrame(), 62 | message, "Info", JOptionPane.DEFAULT_OPTION, messageType); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /git-designer/src/main/java/com/axone_io/ignition/git/utils/IconUtils.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.utils; 2 | 3 | import com.axone_io.ignition.git.DesignerHook; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.imageio.ImageIO; 8 | import javax.swing.*; 9 | import java.awt.image.BufferedImage; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | 13 | public class IconUtils { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(IconUtils.class); 16 | 17 | public static Icon getIcon(String bundleKey){ 18 | InputStream iconStream = DesignerHook.class.getResourceAsStream(bundleKey); 19 | BufferedImage buffer = null; 20 | try { 21 | buffer = ImageIO.read(iconStream); 22 | } catch (IOException e) { 23 | logger.warn(e.toString(), e); 24 | } 25 | return buffer != null ? new ImageIcon(buffer) : null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/DesignerHook.properties: -------------------------------------------------------------------------------- 1 | Toolbar.Name=Git Toolbar 2 | 3 | Actions.Push.Desc=Push all committed changes 4 | Actions.Push.Name=Push 5 | Actions.Push.ConfirmMessage=Everything went well during the Push. 6 | 7 | Actions.Pull.Desc=Pull changes from a remote repository into the current branch 8 | Actions.Pull.Name=Pull 9 | Actions.Pull.ConfirmMessage=Everything went well during the Pull. 10 | 11 | Actions.Commit.Desc=Commit - Record changes to the repository 12 | Actions.Commit.Name=Commit 13 | Actions.Commit.ConfirmMessage=Everything went well during the Commit. 14 | 15 | Actions.ExportGatewayConfig.Desc=Export the gateway configuration to the project folder 16 | Actions.ExportGatewayConfig.Name=ExportGatewayConfig 17 | Actions.ExportGatewayConfig.ConfirmMessage=Everything went well during the GatewayConfigExport. 18 | 19 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_commit.svg: -------------------------------------------------------------------------------- 1 | ic_commit 2 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_folder.svg: -------------------------------------------------------------------------------- 1 | ic_folder 2 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_git.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_pull.svg: -------------------------------------------------------------------------------- 1 | ic_pull 2 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_push.svg: -------------------------------------------------------------------------------- 1 | ic_push 2 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_unregister_user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /git-designer/src/main/resources/com/axone_io/ignition/git/icons/ic_verified_user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /git-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | git 8 | com.axone_io.ignition 9 | 1.0.2 10 | 11 | 12 | git-gateway 13 | 14 | 15 | 16 | com.axone_io.ignition 17 | git-common 18 | ${project.version} 19 | 20 | 21 | com.inductiveautomation.ignitionsdk 22 | ignition-common 23 | ${ignition-sdk-version} 24 | pom 25 | provided 26 | 27 | 28 | com.inductiveautomation.ignitionsdk 29 | gateway-api 30 | ${ignition-sdk-version} 31 | pom 32 | provided 33 | 34 | 35 | org.eclipse.jgit 36 | org.eclipse.jgit.ssh.jsch 37 | 6.5.0.202303070854-r 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-compiler-plugin 46 | 3.2 47 | 48 | ${ignition-java-version} 49 | ${ignition-java-version} 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/GatewayHook.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.axone_io.ignition.git.commissioning.utils.GitCommissioningUtils; 4 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 5 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 6 | import com.axone_io.ignition.git.web.GitProjectsConfigPage; 7 | import com.inductiveautomation.ignition.common.BundleUtil; 8 | import com.inductiveautomation.ignition.common.licensing.LicenseState; 9 | import com.inductiveautomation.ignition.gateway.clientcomm.ClientReqSession; 10 | import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook; 11 | import com.inductiveautomation.ignition.gateway.model.GatewayContext; 12 | import com.inductiveautomation.ignition.gateway.web.models.ConfigCategory; 13 | import com.inductiveautomation.ignition.gateway.web.models.IConfigTab; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.sql.SQLException; 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | public class GatewayHook extends AbstractGatewayModuleHook { 22 | static public String MODULE_NAME = "Git"; 23 | private final Logger logger = LoggerFactory.getLogger(getClass()); 24 | 25 | private GatewayScriptModule scriptModule; 26 | public static GatewayContext context; 27 | 28 | public static final ConfigCategory CONFIG_CATEGORY = 29 | new ConfigCategory(MODULE_NAME, "bundle_git.Config.Git.MenuTitle", 700); 30 | 31 | @Override 32 | public List getConfigPanels() { 33 | return List.of(GitProjectsConfigPage.MENU_ENTRY); 34 | } 35 | 36 | @Override 37 | public List getConfigCategories() { 38 | return Collections.singletonList(CONFIG_CATEGORY); 39 | } 40 | 41 | 42 | @Override 43 | public void setup(GatewayContext gatewayContext) { 44 | context = gatewayContext; 45 | scriptModule = new GatewayScriptModule(context); 46 | BundleUtil.get().addBundle("bundle_git", getClass(), "bundle_git"); 47 | verifySchema(gatewayContext); 48 | 49 | logger.info("setup()"); 50 | } 51 | 52 | private void verifySchema(GatewayContext context) { 53 | try { 54 | context.getSchemaUpdater().updatePersistentRecords(GitProjectsConfigRecord.META, GitReposUsersRecord.META); 55 | } catch (SQLException e) { 56 | logger.error("Error verifying persistent record schemas for HomeConnect records.", e); 57 | } 58 | } 59 | 60 | @Override 61 | public void startup(LicenseState licenseState) { 62 | GitCommissioningUtils.loadConfiguration(); 63 | 64 | logger.info("startup()"); 65 | } 66 | 67 | @Override 68 | public void shutdown() { 69 | logger.info("shutdown()"); 70 | } 71 | 72 | @Override 73 | public boolean isFreeModule() { 74 | return true; 75 | } 76 | 77 | @Override 78 | public boolean isMakerEditionCompatible() { 79 | return true; 80 | } 81 | 82 | @Override 83 | public Object getRPCHandler(ClientReqSession session, String projectName) { 84 | return scriptModule; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/GatewayScriptModule.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.axone_io.ignition.git.commissioning.utils.GitCommissioningUtils; 4 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 5 | import com.inductiveautomation.ignition.common.BasicDataset; 6 | import com.inductiveautomation.ignition.common.Dataset; 7 | import com.inductiveautomation.ignition.common.util.DatasetBuilder; 8 | import com.inductiveautomation.ignition.common.util.LoggerEx; 9 | import com.inductiveautomation.ignition.gateway.model.GatewayContext; 10 | import org.eclipse.jgit.api.*; 11 | import org.eclipse.jgit.api.errors.GitAPIException; 12 | import org.eclipse.jgit.transport.PushResult; 13 | import org.eclipse.jgit.transport.RefSpec; 14 | import org.eclipse.jgit.transport.URIish; 15 | 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Set; 21 | 22 | import static com.axone_io.ignition.git.managers.GitImageManager.exportImages; 23 | import static com.axone_io.ignition.git.managers.GitManager.*; 24 | import static com.axone_io.ignition.git.managers.GitTagManager.exportTag; 25 | import static com.axone_io.ignition.git.managers.GitThemeManager.exportTheme; 26 | 27 | public class GatewayScriptModule extends AbstractScriptModule { 28 | private final LoggerEx logger = LoggerEx.newBuilder().build(getClass()); 29 | private final GatewayContext context; 30 | 31 | GatewayScriptModule(GatewayContext context) { 32 | this.context = context; 33 | } 34 | 35 | @Override 36 | public boolean pullImpl(String projectName, String userName) throws Exception { 37 | try (Git git = getGit(getProjectFolderPath(projectName))) { 38 | PullCommand pull = git.pull(); 39 | setAuthentication(pull, projectName, userName); 40 | 41 | PullResult result = pull.call(); 42 | if (!result.isSuccessful()) { 43 | logger.warn("Cannot pull from git"); 44 | } else { 45 | logger.info("Pull was successful."); 46 | } 47 | 48 | } catch (GitAPIException e) { 49 | logger.error(e.toString()); 50 | throw new RuntimeException(e); 51 | } 52 | return true; 53 | } 54 | 55 | @Override 56 | public boolean pushImpl(String projectName, String userName) throws Exception { 57 | try (Git git = getGit(getProjectFolderPath(projectName))) { 58 | PushCommand push = git.push(); 59 | 60 | setAuthentication(push, projectName, userName); 61 | 62 | Iterable results = push.setPushAll().setPushTags().call(); 63 | for (PushResult result : results) { 64 | logger.trace(result.getMessages()); 65 | } 66 | 67 | } catch (GitAPIException e) { 68 | logger.error(e.toString(), e); 69 | throw new RuntimeException(e); 70 | } 71 | return true; 72 | } 73 | 74 | @Override 75 | protected boolean commitImpl(String projectName, String userName, List changes, String message) { 76 | try (Git git = getGit(getProjectFolderPath(projectName))) { 77 | for (String change : changes) { 78 | git.add().addFilepattern(change).call(); 79 | git.add().setUpdate(true).addFilepattern(change).call(); 80 | } 81 | 82 | CommitCommand commit = git.commit().setMessage(message); 83 | setCommitAuthor(commit, projectName, userName); 84 | commit.call(); 85 | } catch (GitAPIException e) { 86 | logger.error(e.toString(), e); 87 | throw new RuntimeException(e); 88 | } 89 | return true; 90 | } 91 | 92 | @Override 93 | public Dataset getUncommitedChangesImpl(String projectName, String userName) { 94 | Path projectPath = getProjectFolderPath(projectName); 95 | Dataset ds; 96 | List changes = new ArrayList<>(); 97 | DatasetBuilder builder = new DatasetBuilder(); 98 | builder.colNames(List.of("resource", "type", "actor")); 99 | builder.colTypes(List.of(String.class, String.class, String.class)); 100 | 101 | try (Git git = getGit(projectPath)) { 102 | Status status = git.status().call(); 103 | 104 | Set missing = status.getMissing(); 105 | uncommittedChangesBuilder(projectName, missing, "Deleted", changes, builder); 106 | 107 | Set uncommittedChanges = status.getUncommittedChanges(); 108 | uncommittedChangesBuilder(projectName, uncommittedChanges, "Uncommitted", changes, builder); 109 | 110 | Set untracked = status.getUntracked(); 111 | uncommittedChangesBuilder(projectName, untracked, "Created", changes, builder); 112 | 113 | } catch (Exception e) { 114 | logger.info(e.toString(), e); 115 | } 116 | ds = builder.build(); 117 | 118 | return ds != null ? ds : new BasicDataset(); 119 | } 120 | 121 | @Override 122 | public boolean isRegisteredUserImpl(String projectName, String userName) { 123 | boolean registered; 124 | try { 125 | GitProjectsConfigRecord gitProjectsConfigRecord = getGitProjectConfigRecord(projectName); 126 | getGitReposUserRecord(gitProjectsConfigRecord, userName); 127 | registered = true; 128 | } catch (Exception e) { 129 | registered = false; 130 | } 131 | return registered; 132 | } 133 | 134 | @Override 135 | protected boolean exportConfigImpl(String projectName) { 136 | Path projectFolderPath = getProjectFolderPath(projectName); 137 | exportImages(projectFolderPath); 138 | exportTheme(projectFolderPath); 139 | exportTag(projectFolderPath); 140 | return true; 141 | } 142 | 143 | @Override 144 | public void setupLocalRepoImpl(String projectName, String userName) throws Exception { 145 | Path projectFolderPath = getProjectFolderPath(projectName); 146 | GitProjectsConfigRecord gitProjectsConfigRecord = getGitProjectConfigRecord(projectName); 147 | 148 | Path path = projectFolderPath.resolve(".git"); 149 | 150 | if (!Files.exists(path)) { 151 | String defaultBranch = GitCommissioningUtils.config != null ? GitCommissioningUtils.config.getInitDefaultBranch() : null; 152 | try (Git git = Git.init().setInitialBranch(defaultBranch).setDirectory(projectFolderPath.toFile()).call()) { 153 | disableSsl(git); 154 | 155 | git.remoteAdd().setName("origin").setUri(new URIish(gitProjectsConfigRecord.getURI())).call(); 156 | 157 | git.add().addFilepattern(".").call(); 158 | 159 | CommitCommand commit = git.commit().setMessage("Initial commit"); 160 | setCommitAuthor(commit, projectName, userName); 161 | commit.call(); 162 | 163 | PushCommand pushCommand = git.push(); 164 | 165 | setAuthentication(pushCommand, projectName, userName); 166 | 167 | String branch = git.getRepository().getBranch(); 168 | pushCommand.setRemote("origin").setRefSpecs(new RefSpec(branch)).call(); 169 | } catch (Exception e) { 170 | logger.warn("An error occurred while setting up local repo for '" + projectName + "' project."); 171 | } 172 | } 173 | } 174 | 175 | private Path getProjectFolderPath(String projectName) { 176 | Path dataDir = context.getSystemManager().getDataDir().toPath(); 177 | return dataDir.resolve("projects").resolve(projectName); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/SshTransportConfigCallback.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git; 2 | 3 | import com.jcraft.jsch.JSch; 4 | import com.jcraft.jsch.JSchException; 5 | import com.jcraft.jsch.Session; 6 | import org.eclipse.jgit.api.TransportConfigCallback; 7 | import org.eclipse.jgit.transport.SshSessionFactory; 8 | import org.eclipse.jgit.transport.SshTransport; 9 | import org.eclipse.jgit.transport.Transport; 10 | import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory; 11 | import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig; 12 | import org.eclipse.jgit.util.FS; 13 | 14 | public class SshTransportConfigCallback implements TransportConfigCallback { 15 | String sshKey; 16 | 17 | public SshTransportConfigCallback(String sshKey) { 18 | this.sshKey = sshKey; 19 | } 20 | 21 | private final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() { 22 | @Override 23 | protected void configure(OpenSshConfig.Host hc, Session session) { 24 | session.setConfig("StrictHostKeyChecking", "no"); 25 | } 26 | 27 | @Override 28 | protected JSch createDefaultJSch(FS fs) throws JSchException { 29 | JSch jSch = super.createDefaultJSch(fs); 30 | jSch.addIdentity("identity", sshKey.getBytes(), null, null); 31 | return jSch; 32 | } 33 | }; 34 | 35 | @Override 36 | public void configure(Transport transport) { 37 | SshTransport sshTransport = (SshTransport) transport; 38 | sshTransport.setSshSessionFactory(sshSessionFactory); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/commissioning/GitCommissioningConfig.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.commissioning; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | 8 | public class GitCommissioningConfig { 9 | private String repoURI; 10 | private String repoBranch; 11 | private String ignitionProjectName; 12 | private String ignitionUserName; 13 | private String userName; 14 | private String userPassword; 15 | private String sshKey; 16 | private String userEmail; 17 | 18 | private boolean importImages = false; 19 | private boolean importTags = false; 20 | private boolean importThemes = false; 21 | 22 | private String initDefaultBranch; 23 | 24 | public String getRepoURI() { 25 | return repoURI; 26 | } 27 | 28 | public void setRepoURI(String repoURI) { 29 | this.repoURI = repoURI; 30 | } 31 | 32 | public String getRepoBranch() { 33 | return repoBranch; 34 | } 35 | 36 | public void setRepoBranch(String repoBranch) { 37 | this.repoBranch = repoBranch; 38 | } 39 | 40 | public String getIgnitionProjectName() { 41 | return ignitionProjectName; 42 | } 43 | 44 | public void setIgnitionProjectName(String ignitionProjectName) { 45 | this.ignitionProjectName = ignitionProjectName; 46 | } 47 | 48 | public String getUserName() { 49 | return userName; 50 | } 51 | 52 | public void setUserName(String userName) { 53 | this.userName = userName; 54 | } 55 | 56 | public String getUserPassword() { 57 | return userPassword; 58 | } 59 | 60 | public void setUserPassword(String userPassword) { 61 | this.userPassword = userPassword; 62 | } 63 | 64 | public String getUserEmail() { 65 | return userEmail; 66 | } 67 | 68 | public void setUserEmail(String userEmail) { 69 | this.userEmail = userEmail; 70 | } 71 | 72 | public String getIgnitionUserName() { 73 | return ignitionUserName; 74 | } 75 | 76 | public void setIgnitionUserName(String ignitionUserName) { 77 | this.ignitionUserName = ignitionUserName; 78 | } 79 | 80 | public boolean isImportImages() { 81 | return importImages; 82 | } 83 | 84 | public void setImportImages(boolean importImages) { 85 | this.importImages = importImages; 86 | } 87 | 88 | public boolean isImportTags() { 89 | return importTags; 90 | } 91 | 92 | public void setImportTags(boolean importTags) { 93 | this.importTags = importTags; 94 | } 95 | 96 | public boolean isImportThemes() { 97 | return importThemes; 98 | } 99 | 100 | public void setImportThemes(boolean importThemes) { 101 | this.importThemes = importThemes; 102 | } 103 | 104 | public String getSshKey() { 105 | return sshKey; 106 | } 107 | 108 | public void setSshKey(String sshKey) { 109 | this.sshKey = sshKey; 110 | } 111 | 112 | public void setSecretFromFilePath(Path filePath, boolean isSSHAuth) throws IOException { 113 | if (filePath.toFile().exists() && filePath.toFile().isFile()) { 114 | String secret = Files.readString(filePath, StandardCharsets.UTF_8); 115 | if (isSSHAuth) { 116 | this.sshKey = secret; 117 | } else { 118 | this.userPassword = secret; 119 | } 120 | } 121 | } 122 | 123 | public String getInitDefaultBranch() { 124 | return initDefaultBranch; 125 | } 126 | 127 | public void setInitDefaultBranch(String initDefaultBranch) { 128 | this.initDefaultBranch = initDefaultBranch; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/commissioning/utils/GitCommissioningUtils.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.commissioning.utils; 2 | 3 | import com.axone_io.ignition.git.commissioning.GitCommissioningConfig; 4 | import com.axone_io.ignition.git.managers.GitImageManager; 5 | import com.axone_io.ignition.git.managers.GitProjectManager; 6 | import com.axone_io.ignition.git.managers.GitTagManager; 7 | import com.axone_io.ignition.git.managers.GitThemeManager; 8 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 9 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 10 | import com.google.common.eventbus.Subscribe; 11 | import com.inductiveautomation.ignition.common.project.ProjectManifest; 12 | import com.inductiveautomation.ignition.common.util.LoggerEx; 13 | import com.inductiveautomation.ignition.gateway.localdb.persistence.PersistenceInterface; 14 | import com.inductiveautomation.ignition.gateway.project.ProjectManager; 15 | import org.apache.commons.io.FileUtils; 16 | import simpleorm.dataset.SQuery; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.ByteArrayInputStream; 20 | import java.io.File; 21 | import java.io.InputStreamReader; 22 | import java.nio.charset.StandardCharsets; 23 | import java.nio.file.Path; 24 | import java.nio.file.Paths; 25 | import java.util.ArrayList; 26 | import java.util.regex.Matcher; 27 | import java.util.regex.Pattern; 28 | 29 | import static com.axone_io.ignition.git.GatewayHook.context; 30 | import static com.axone_io.ignition.git.managers.GitManager.*; 31 | 32 | public class GitCommissioningUtils { 33 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitCommissioningUtils.class); 34 | 35 | public static GitCommissioningConfig config; 36 | 37 | @Subscribe 38 | public static void loadConfiguration() { 39 | Path dataDir = getDataFolderPath(); 40 | File ignitionConf = dataDir.resolve("git.conf").toFile(); 41 | ProjectManager projectManager = context.getProjectManager(); 42 | 43 | try { 44 | if (ignitionConf.exists() && ignitionConf.isFile()) { 45 | config = parseConfigLines(FileUtils.readFileToByteArray(ignitionConf)); 46 | if (projectManager.getProjectNames().contains(config.getIgnitionProjectName())) { 47 | logger.info("The configuration of the git module was interrupted because the project '" + config.getIgnitionProjectName() + "' already exist."); 48 | return; 49 | } 50 | 51 | if (config.getRepoURI() == null || config.getRepoBranch() == null 52 | || config.getIgnitionProjectName() == null || config.getIgnitionUserName() == null 53 | || config.getUserName() == null || (config.getUserPassword() == null && config.getSshKey() == null) 54 | || config.getUserEmail() == null) { 55 | throw new RuntimeException("Incomplete git configuration file."); 56 | } 57 | 58 | projectManager.createProject(config.getIgnitionProjectName(), new ProjectManifest(config.getIgnitionProjectName(), "", false, false, ""), new ArrayList()); 59 | 60 | Path projectDir = getProjectFolderPath(config.getIgnitionProjectName()); 61 | clearDirectory(projectDir); 62 | 63 | // Creation of records 64 | PersistenceInterface persistenceInterface = context.getPersistenceInterface(); 65 | SQuery query = new SQuery<>(GitProjectsConfigRecord.META).eq(GitProjectsConfigRecord.ProjectName, config.getIgnitionProjectName()); 66 | if (persistenceInterface.queryOne(query) != null) { 67 | logger.info("The configuration of the git module was interrupted because the GitProjectsConfigRecord '" + config.getIgnitionProjectName() + "' already exist."); 68 | return; 69 | } 70 | GitProjectsConfigRecord projectsConfigRecord = persistenceInterface.createNew(GitProjectsConfigRecord.META); 71 | projectsConfigRecord.setProjectName(config.getIgnitionProjectName()); 72 | projectsConfigRecord.setURI(config.getRepoURI()); 73 | 74 | String userSecretFilePath = System.getenv("GATEWAY_GIT_USER_SECRET_FILE"); 75 | if (userSecretFilePath != null) { 76 | config.setSecretFromFilePath(Paths.get(userSecretFilePath), projectsConfigRecord.isSSHAuthentication()); 77 | } 78 | if (config.getSshKey() == null && config.getUserPassword() == null) { 79 | throw new Exception("Git User Password or SSHKey not configured."); 80 | } 81 | persistenceInterface.save(projectsConfigRecord); 82 | 83 | GitReposUsersRecord reposUsersRecord = persistenceInterface.createNew(GitReposUsersRecord.META); 84 | reposUsersRecord.setUserName(config.getUserName()); 85 | reposUsersRecord.setIgnitionUser(config.getIgnitionUserName()); 86 | reposUsersRecord.setProjectId(projectsConfigRecord.getId()); 87 | if (projectsConfigRecord.isSSHAuthentication()) { 88 | reposUsersRecord.setSSHKey(config.getSshKey()); 89 | } else { 90 | reposUsersRecord.setPassword(config.getUserPassword()); 91 | } 92 | reposUsersRecord.setEmail(config.getUserEmail()); 93 | persistenceInterface.save(reposUsersRecord); 94 | 95 | // CLONE PROJECT 96 | cloneRepo(config.getIgnitionProjectName(), config.getIgnitionUserName(), config.getRepoURI(), config.getRepoBranch()); 97 | 98 | // IMPORT PROJECT 99 | GitProjectManager.importProject(config.getIgnitionProjectName()); 100 | 101 | // IMPORT TAGS 102 | if (config.isImportTags()) { 103 | GitTagManager.importTagManager(config.getIgnitionProjectName()); 104 | } 105 | 106 | // IMPORT THEMES 107 | if (config.isImportThemes()) { 108 | GitThemeManager.importTheme(config.getIgnitionProjectName()); 109 | } 110 | 111 | // IMPORT IMAGES 112 | if (config.isImportImages()) { 113 | GitImageManager.importImages(config.getIgnitionProjectName()); 114 | } 115 | } else { 116 | logger.info("No git configuration file was found."); 117 | } 118 | 119 | } catch (Exception e) { 120 | logger.error("An error occurred while git configuration settings up.", e); 121 | } 122 | } 123 | 124 | 125 | static protected GitCommissioningConfig parseConfigLines(byte[] ignitionConf) { 126 | Pattern repoUriPattern = Pattern.compile("repo.uri"); 127 | Pattern repoBranchPattern = Pattern.compile("repo.branch"); 128 | 129 | Pattern projectNamePattern = Pattern.compile("ignition.project.name"); 130 | Pattern ignitionUserName = Pattern.compile("ignition.user.name"); 131 | 132 | Pattern userNamePattern = Pattern.compile("user.name"); 133 | Pattern passwordPattern = Pattern.compile("user.password"); 134 | Pattern emailPattern = Pattern.compile("user.email"); 135 | Pattern sshKeyFilePath = Pattern.compile("user.shh.key.file.path"); 136 | 137 | Pattern importTags = Pattern.compile("commissioning.import.tags"); 138 | Pattern importThemes = Pattern.compile("commissioning.import.themes"); 139 | Pattern importImages = Pattern.compile("commissioning.import.images"); 140 | 141 | Pattern initDefaultBranch = Pattern.compile("init.defaultBranch"); 142 | 143 | GitCommissioningConfig config = new GitCommissioningConfig(); 144 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(ignitionConf), StandardCharsets.UTF_8))) { 145 | String line; 146 | while ((line = reader.readLine()) != null) { 147 | Matcher repoUriMatcher = repoUriPattern.matcher(line); 148 | Matcher repoBranchMatcher = repoBranchPattern.matcher(line); 149 | Matcher projectNameMatcher = projectNamePattern.matcher(line); 150 | Matcher userNameMatcher = userNamePattern.matcher(line); 151 | Matcher passwordMatcher = passwordPattern.matcher(line); 152 | Matcher emailMatcher = emailPattern.matcher(line); 153 | Matcher ignitionUserNameMatcher = ignitionUserName.matcher(line); 154 | Matcher importTagsMatcher = importTags.matcher(line); 155 | Matcher importThemesMatcher = importThemes.matcher(line); 156 | Matcher importImagesMatcher = importImages.matcher(line); 157 | Matcher sshKeyFilePathMatcher = sshKeyFilePath.matcher(line); 158 | Matcher initDefaultBranchPathMatcher = initDefaultBranch.matcher(line); 159 | 160 | if (repoUriMatcher.find()) { 161 | config.setRepoURI(line.split("=")[1]); 162 | } else if (repoBranchMatcher.find()) { 163 | config.setRepoBranch(line.split("=")[1]); 164 | } else if (projectNameMatcher.find()) { 165 | config.setIgnitionProjectName(line.split("=")[1]); 166 | } else if (ignitionUserNameMatcher.find()) { 167 | config.setIgnitionUserName(line.split("=")[1]); 168 | } else if (userNameMatcher.find()) { 169 | config.setUserName(line.split("=")[1]); 170 | } else if (passwordMatcher.find()) { 171 | config.setUserPassword(line.split("=")[1]); 172 | } else if (emailMatcher.find()) { 173 | config.setUserEmail(line.split("=")[1]); 174 | } else if (importTagsMatcher.find()) { 175 | config.setImportTags(Boolean.parseBoolean(line.split("=")[1])); 176 | } else if (importThemesMatcher.find()) { 177 | config.setImportThemes(Boolean.parseBoolean(line.split("=")[1])); 178 | } else if (importImagesMatcher.find()) { 179 | config.setImportImages(Boolean.parseBoolean(line.split("=")[1])); 180 | } else if (sshKeyFilePathMatcher.find()) { 181 | config.setSecretFromFilePath(Paths.get(line.split("=")[1]), true); 182 | } else if (initDefaultBranchPathMatcher.find()) { 183 | config.setInitDefaultBranch(line.split("=")[1]); 184 | } 185 | } 186 | } catch (ArrayIndexOutOfBoundsException e) { 187 | throw new RuntimeException("Invalid git configuration file.", e); 188 | } catch (Exception e) { 189 | logger.error("An error occurred while importing the Git configuration file.", e); 190 | throw new RuntimeException(e); 191 | } 192 | return config; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/managers/GitImageManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.inductiveautomation.ignition.common.util.LoggerEx; 4 | import com.inductiveautomation.ignition.gateway.images.ImageFormat; 5 | import com.inductiveautomation.ignition.gateway.images.ImageManager; 6 | import com.inductiveautomation.ignition.gateway.images.ImageRecord; 7 | import com.inductiveautomation.ignition.gateway.localdb.persistence.PersistenceInterface; 8 | import simpleorm.dataset.SQuery; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.awt.image.BufferedImage; 13 | import java.io.File; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.List; 19 | 20 | import static com.axone_io.ignition.git.GatewayHook.context; 21 | import static com.axone_io.ignition.git.managers.GitManager.clearDirectory; 22 | import static com.axone_io.ignition.git.managers.GitManager.getProjectFolderPath; 23 | 24 | public class GitImageManager { 25 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitImageManager.class); 26 | 27 | public static void importImages(String projectName) { 28 | Path projectDir = getProjectFolderPath(projectName); 29 | File directory = projectDir.resolve("images").toFile(); 30 | 31 | // DELETION 32 | PersistenceInterface persistenceInterface = context.getPersistenceInterface(); 33 | List images = persistenceInterface.query(new SQuery<>(ImageRecord.META)); 34 | images.forEach(i -> { 35 | i.deleteRecord(); 36 | persistenceInterface.save(i); 37 | }); 38 | 39 | // INSERTION 40 | File[] files = directory.listFiles(); 41 | uploadFiles(files != null ? files : new File[0]); 42 | } 43 | 44 | protected static void uploadFiles(File[] files) { 45 | for (File file : files) { 46 | if (file.isDirectory()) { 47 | uploadFolder(file, ""); 48 | } else { 49 | uploadFile(file, ""); 50 | } 51 | } 52 | } 53 | 54 | 55 | protected static void uploadFile(File f, String path) { 56 | String lName = f.getName().toLowerCase(); 57 | if (lName.endsWith(".png") || lName 58 | .endsWith(".gif") || lName 59 | .endsWith(".jpg") || lName 60 | .endsWith(".jpeg") || lName 61 | .endsWith(".svg")) 62 | try { 63 | String ext = lName.substring(lName.lastIndexOf(".") + 1).toUpperCase(); 64 | byte[] bytes = Files.readAllBytes(f.toPath()); 65 | int width = 0; 66 | int height = 0; 67 | Image img = Toolkit.getDefaultToolkit().createImage(bytes); 68 | if (PathIcon.waitForImage(img)) { 69 | width = img.getWidth(null); 70 | height = img.getHeight(null); 71 | } 72 | 73 | try { 74 | context.getImageManager().insertImage(f.getName(), "", ImageFormat.valueOf(ext), path, bytes, width, height, bytes.length); 75 | } catch (Exception ex) { 76 | logger.error(ex.getMessage(), ex); 77 | } 78 | } catch (FileNotFoundException e) { 79 | logger.error("FileNotFound exception for file: '" + f.getPath() + "'"); 80 | } catch (IOException e) { 81 | logger.error("IOException exception for file: '" + f.getPath() + "'"); 82 | } catch (Exception e) { 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | 87 | 88 | protected static void uploadFolder(File dir, String path) { 89 | try { 90 | context.getImageManager().insertImageFolder(dir.getName(), path.equals("") ? null : path); 91 | File[] files = dir.listFiles(); 92 | if (files != null) { 93 | for (File file : files) { 94 | if (file.isDirectory()) { 95 | uploadFolder(file, path + dir.getName() + "/"); 96 | } else { 97 | uploadFile(file, path + dir.getName() + "/"); 98 | } 99 | } 100 | } 101 | } catch (Exception ex) { 102 | logger.error(ex.getMessage(), ex); 103 | } 104 | } 105 | 106 | public static void exportImages(Path projectFolderPath) { 107 | Path imageFolderPath = projectFolderPath.resolve("images"); 108 | clearDirectory(imageFolderPath); 109 | try { 110 | Files.createDirectories(imageFolderPath); 111 | } catch (IOException e) { 112 | logger.error(e.toString(), e); 113 | } 114 | saveFolderImage(imageFolderPath, ""); 115 | } 116 | 117 | public static void saveFolderImage(Path folderPath, String directory) { 118 | ImageManager imageManager = context.getImageManager(); 119 | for (ImageRecord imageRecord : imageManager.getImages(directory)) { 120 | String path = imageRecord.getString(ImageRecord.Path); 121 | if (imageRecord.isDirectory()) { 122 | try { 123 | Files.createDirectories(folderPath.resolve(path)); 124 | } catch (IOException e) { 125 | logger.error(e.toString(), e); 126 | } 127 | 128 | saveFolderImage(folderPath, path); 129 | } else { 130 | byte[] data = imageManager.getImage(path).getBytes(ImageRecord.Data); 131 | 132 | try { 133 | Files.write(folderPath.resolve(path), data); 134 | } catch (IOException e) { 135 | logger.error(e.toString(), e); 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | class PathIcon extends ImageIcon { 143 | protected static final Component COMP = new Component() { 144 | }; 145 | private static MediaTracker tracker; 146 | private static int nextId; 147 | public static boolean waitForImage(Image image) { 148 | if (image == null) { 149 | return false; 150 | } else if (image instanceof BufferedImage) { 151 | return true; 152 | } else { 153 | int id; 154 | synchronized(COMP) { 155 | id = nextId++; 156 | } 157 | 158 | tracker.addImage(image, id); 159 | 160 | try { 161 | tracker.waitForID(id); 162 | } catch (InterruptedException var4) { 163 | System.err.println("Image loading interrupted!"); 164 | return false; 165 | } 166 | 167 | boolean success = !tracker.isErrorID(id); 168 | tracker.removeImage(image, id); 169 | return success; 170 | } 171 | } 172 | 173 | static { 174 | tracker = new MediaTracker(COMP); 175 | nextId = 0; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/managers/GitManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.axone_io.ignition.git.SshTransportConfigCallback; 4 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 5 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 6 | import com.inductiveautomation.ignition.common.gson.Gson; 7 | import com.inductiveautomation.ignition.common.gson.JsonElement; 8 | import com.inductiveautomation.ignition.common.gson.JsonObject; 9 | import com.inductiveautomation.ignition.common.project.RuntimeProject; 10 | import com.inductiveautomation.ignition.common.project.resource.LastModification; 11 | import com.inductiveautomation.ignition.common.project.resource.ProjectResource; 12 | import com.inductiveautomation.ignition.common.project.resource.ResourcePath; 13 | import com.inductiveautomation.ignition.common.project.resource.ResourceType; 14 | import com.inductiveautomation.ignition.common.util.DatasetBuilder; 15 | import com.inductiveautomation.ignition.common.util.LoggerEx; 16 | import com.inductiveautomation.ignition.gateway.project.ProjectManager; 17 | import org.apache.commons.io.FileUtils; 18 | import org.eclipse.jgit.api.*; 19 | import org.eclipse.jgit.lib.ObjectId; 20 | import org.eclipse.jgit.lib.ObjectReader; 21 | import org.eclipse.jgit.lib.Repository; 22 | import org.eclipse.jgit.lib.StoredConfig; 23 | import org.eclipse.jgit.revwalk.RevCommit; 24 | import org.eclipse.jgit.revwalk.RevTree; 25 | import org.eclipse.jgit.revwalk.RevWalk; 26 | import org.eclipse.jgit.transport.RefSpec; 27 | import org.eclipse.jgit.transport.URIish; 28 | import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; 29 | import org.eclipse.jgit.treewalk.TreeWalk; 30 | import org.eclipse.jgit.treewalk.filter.PathFilter; 31 | import simpleorm.dataset.SQuery; 32 | 33 | import java.io.ByteArrayOutputStream; 34 | import java.io.File; 35 | import java.io.IOException; 36 | import java.nio.file.Files; 37 | import java.nio.file.Path; 38 | import java.nio.file.Paths; 39 | import java.util.ArrayList; 40 | import java.util.Arrays; 41 | import java.util.List; 42 | import java.util.Set; 43 | 44 | import static com.axone_io.ignition.git.GatewayHook.context; 45 | 46 | public class GitManager { 47 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitManager.class); 48 | 49 | static public Git getGit(Path projectFolderPath) { 50 | Git git; 51 | try { 52 | git = Git.open(projectFolderPath.resolve(".git").toFile()); 53 | disableSsl(git); 54 | } catch (IOException e) { 55 | logger.error("Unable to retrieve Git repository", e); 56 | throw new RuntimeException(e); 57 | } 58 | return git; 59 | } 60 | 61 | public static Path getProjectFolderPath(String projectName) { 62 | Path dataDir = getDataFolderPath(); 63 | return dataDir.resolve("projects").resolve(projectName); 64 | } 65 | 66 | public static Path getDataFolderPath() { 67 | return context.getSystemManager().getDataDir().toPath(); 68 | } 69 | 70 | 71 | public static void clearDirectory(Path folderPath) { 72 | try { 73 | if (folderPath.toFile().exists()) { 74 | FileUtils.cleanDirectory(folderPath.toFile()); 75 | } 76 | } catch (Exception e) { 77 | logger.error(e.toString(), e); 78 | } 79 | } 80 | 81 | public static void setAuthentication(TransportCommand command, String projectName, String userName) throws Exception { 82 | GitProjectsConfigRecord gitProjectsConfigRecord = getGitProjectConfigRecord(projectName); 83 | GitReposUsersRecord user = getGitReposUserRecord(gitProjectsConfigRecord, userName); 84 | 85 | setAuthentication(command, gitProjectsConfigRecord, user); 86 | } 87 | 88 | public static void setAuthentication(TransportCommand command, GitProjectsConfigRecord gitProjectsConfigRecord, GitReposUsersRecord user) { 89 | 90 | if (gitProjectsConfigRecord.isSSHAuthentication()) { 91 | command.setTransportConfigCallback(getSshTransportConfigCallback(user)); 92 | } else { 93 | command.setCredentialsProvider(getUsernamePasswordCredentialsProvider(user)); 94 | } 95 | } 96 | 97 | 98 | public static void setCommitAuthor(CommitCommand command, String projectName, String userName) { 99 | try { 100 | GitProjectsConfigRecord gitProjectsConfigRecord = getGitProjectConfigRecord(projectName); 101 | GitReposUsersRecord user = getGitReposUserRecord(gitProjectsConfigRecord, userName); 102 | command.setAuthor("", user.getEmail()); 103 | } catch (Exception e) { 104 | logger.error("An error occurred while setting up commit author.", e); 105 | } 106 | 107 | } 108 | 109 | public static GitProjectsConfigRecord getGitProjectConfigRecord(String projectName) throws Exception { 110 | SQuery projectQuery = new SQuery<>(GitProjectsConfigRecord.META) 111 | .eq(GitProjectsConfigRecord.ProjectName, projectName); 112 | GitProjectsConfigRecord gitProjectsConfigRecord = context.getPersistenceInterface().queryOne(projectQuery); 113 | 114 | if (gitProjectsConfigRecord == null) { 115 | throw new Exception("Git Project not configured."); 116 | } 117 | 118 | return gitProjectsConfigRecord; 119 | } 120 | 121 | public static GitReposUsersRecord getGitReposUserRecord(GitProjectsConfigRecord gitProjectsConfigRecord, 122 | String userName) throws Exception { 123 | SQuery userQuery = new SQuery<>(GitReposUsersRecord.META) 124 | .eq(GitReposUsersRecord.ProjectId, gitProjectsConfigRecord.getId()) 125 | .eq(GitReposUsersRecord.IgnitionUser, userName); 126 | GitReposUsersRecord user = context.getPersistenceInterface().queryOne(userQuery); 127 | 128 | if (user == null) { 129 | throw new Exception("Git User not configured."); 130 | } 131 | 132 | return user; 133 | } 134 | 135 | public static UsernamePasswordCredentialsProvider getUsernamePasswordCredentialsProvider(GitReposUsersRecord user) { 136 | return new UsernamePasswordCredentialsProvider(user.getUserName(), user.getPassword()); 137 | } 138 | 139 | public static SshTransportConfigCallback getSshTransportConfigCallback(GitReposUsersRecord user) { 140 | return new SshTransportConfigCallback(user.getSSHKey()); 141 | } 142 | public static int countOccurrences(Set list, String prefix) { 143 | int count = 0; 144 | for (String str : list) { 145 | if (str.startsWith(prefix)) { 146 | count++; 147 | } 148 | } 149 | return count; 150 | } 151 | 152 | public static void uncommittedChangesBuilder(String projectName, 153 | Set updates, 154 | String type, 155 | List changes, 156 | DatasetBuilder builder) { 157 | for (String update : updates) { 158 | String[] rowData = new String[3]; 159 | String actor = "unknown"; 160 | String path = update; 161 | boolean toAdd = true; 162 | 163 | if (hasActor(path)) { 164 | String[] pathSplitted = update.split("/"); 165 | path = String.join("/", Arrays.copyOf(pathSplitted, pathSplitted.length - 1)); 166 | 167 | actor = getActor(projectName, path); 168 | } 169 | 170 | if((update.endsWith("resource.json") && countOccurrences(updates, path) < 2) 171 | || update.endsWith("project.json")){ 172 | if(!isUpdatedResource(projectName, update)){ 173 | toAdd = false; 174 | } 175 | } 176 | 177 | rowData[0] = path; 178 | rowData[1] = type; 179 | if (toAdd && !changes.contains(path)) { 180 | rowData[2] = actor; 181 | changes.add(path); 182 | builder.addRow((Object[]) rowData); 183 | } 184 | } 185 | } 186 | 187 | public static boolean hasActor(String resource) { 188 | boolean hasActor = false; 189 | 190 | if (resource.startsWith("ignition")) { 191 | hasActor = Boolean.TRUE; 192 | } 193 | 194 | if (resource.startsWith("com.inductiveautomation.")) { 195 | hasActor = Boolean.TRUE; 196 | } 197 | 198 | return hasActor; 199 | } 200 | 201 | public static String getActor(String projectName, String path) { 202 | ProjectManager projectManager = context.getProjectManager(); 203 | RuntimeProject project = projectManager.getProject(projectName).get(); 204 | 205 | ProjectResource projectResource = project.getResource(getResourcePath(path)).get(); 206 | return LastModification.of(projectResource).map(LastModification::getActor).orElse("unknown"); 207 | } 208 | 209 | public static List getAddedFiles(String projectName) { 210 | List fileList = new ArrayList<>(); 211 | Git git = getGit(getProjectFolderPath(projectName)); 212 | try { 213 | Status status = git.status().call(); 214 | fileList.addAll(status.getAdded()); 215 | git.close(); 216 | } catch (Exception e) { 217 | logger.info(e.toString(), e); 218 | throw new RuntimeException(e); 219 | } 220 | return fileList; 221 | } 222 | 223 | public static void cloneRepo(String projectName, String userName, String URI, String branchName) { 224 | File projectDirFile = getProjectFolderPath(projectName).toFile(); 225 | if (projectDirFile.exists()) { 226 | try (Git git = Git.init().setDirectory(projectDirFile).call()) { 227 | disableSsl(git); 228 | 229 | // GIT REMOTE ADD 230 | URIish urIish = new URIish(URI); 231 | git.remoteAdd() 232 | .setName(urIish.getHumanishName()) 233 | .setUri(urIish).call(); 234 | 235 | //GIT FETCH 236 | FetchCommand fetch = git.fetch() 237 | .setRemote(urIish.getHumanishName()) 238 | .setRefSpecs(new RefSpec("refs/heads/" + branchName + ":refs/remotes/" + urIish.getHumanishName() + "/" + branchName)); 239 | 240 | setAuthentication(fetch, projectName, userName); 241 | fetch.call(); 242 | 243 | //GIT CHECKOUT 244 | CheckoutCommand checkout = git.checkout() 245 | .setCreateBranch(true) 246 | .setName(branchName) 247 | .setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) 248 | .setStartPoint(urIish.getHumanishName() + "/" + branchName); 249 | checkout.call(); 250 | } catch (Exception e) { 251 | logger.error(e.toString()); 252 | throw new RuntimeException(e); 253 | } 254 | } 255 | } 256 | 257 | 258 | public static ResourcePath getResourcePath(String resourcePath) { 259 | String moduleId = ""; 260 | String typeId = ""; 261 | String resource = ""; 262 | String[] paths = resourcePath.split("/"); 263 | 264 | if (paths.length > 0) moduleId = paths[0]; 265 | if (paths.length > 1) typeId = paths[1]; 266 | if (paths.length > 2) resource = resourcePath.replace(moduleId + "/" + typeId + "/", ""); 267 | 268 | return new ResourcePath(new ResourceType(moduleId, typeId), resource); 269 | } 270 | 271 | public static void disableSsl(Git git) throws IOException { 272 | StoredConfig config = git.getRepository().getConfig(); 273 | config.setBoolean("http", null, "sslVerify", false); 274 | config.save(); 275 | } 276 | 277 | public static boolean isUpdatedResource(String projectName, String resourcePath){ 278 | boolean isUpdatedResource; 279 | Path projectPath = getProjectFolderPath(projectName); 280 | String filePath = projectPath.toAbsolutePath() + "\\" +resourcePath.replace("/", "\\"); 281 | 282 | try (Repository repository = getGit(projectPath).getRepository()) { 283 | 284 | // Get the ObjectId of the latest commit 285 | ObjectId headId = repository.resolve("HEAD"); 286 | 287 | // Use RevWalk to traverse the commit history 288 | try (RevWalk revWalk = new RevWalk(repository)) { 289 | RevCommit commit = revWalk.parseCommit(headId); 290 | 291 | // Get the tree of the commit 292 | RevTree tree = commit.getTree(); 293 | 294 | // Use TreeWalk to traverse the files in the tree 295 | try (TreeWalk treeWalk = new TreeWalk(repository)) { 296 | treeWalk.addTree(tree); 297 | treeWalk.setRecursive(true); 298 | treeWalk.setFilter(PathFilter.create(resourcePath)); 299 | 300 | // Get the ObjectId of the file in the commit 301 | if (!treeWalk.next()) { 302 | throw new IllegalStateException("Did not find expected file " + resourcePath); 303 | } 304 | ObjectId objectId = treeWalk.getObjectId(0); 305 | 306 | // Get the contents of the file in the commit 307 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 308 | try (ObjectReader reader = repository.newObjectReader()) { 309 | reader.open(objectId).copyTo(out); 310 | } 311 | Gson g = new Gson(); 312 | 313 | String contentBefore = out.toString(); 314 | JsonObject jsonBefore = (JsonObject) g.fromJson(contentBefore, JsonElement.class); 315 | jsonBefore.remove("files"); 316 | jsonBefore.remove("attributes"); 317 | 318 | 319 | String contentAfter = new String(Files.readAllBytes(Paths.get(filePath))); 320 | JsonObject jsonAfter = (JsonObject) g.fromJson(contentAfter, JsonElement.class); 321 | jsonAfter.remove("files"); 322 | jsonAfter.remove("attributes"); 323 | 324 | isUpdatedResource = !jsonBefore.equals(jsonAfter); 325 | } 326 | } 327 | } catch (IOException e) { 328 | throw new RuntimeException(e); 329 | } 330 | 331 | return isUpdatedResource; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/managers/GitProjectManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.inductiveautomation.ignition.common.StringPath; 4 | import com.inductiveautomation.ignition.common.gson.JsonSyntaxException; 5 | import com.inductiveautomation.ignition.common.project.ProjectInvalidException; 6 | import com.inductiveautomation.ignition.common.project.ProjectManifest; 7 | import com.inductiveautomation.ignition.common.project.resource.*; 8 | import com.inductiveautomation.ignition.common.util.LoggerEx; 9 | import com.inductiveautomation.ignition.gateway.project.ProjectManager; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.nio.charset.StandardCharsets; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.util.*; 18 | import java.util.stream.Collectors; 19 | 20 | import static com.axone_io.ignition.git.GatewayHook.context; 21 | import static com.axone_io.ignition.git.managers.GitManager.getProjectFolderPath; 22 | 23 | public class GitProjectManager { 24 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitProjectManager.class); 25 | 26 | public static void importProject(String projectName) { 27 | ProjectManager projectManager = context.getProjectManager(); 28 | Path projectDir = getProjectFolderPath(projectName); 29 | 30 | try { 31 | Set resources = importFromFolder(projectDir, projectName); 32 | ProjectManifest projectManifest = loadProjectManifest(projectDir); 33 | projectManager.createOrReplaceProject(projectName, projectManifest, new ArrayList(resources)); 34 | 35 | } catch (ProjectInvalidException | IOException e) { 36 | logger.error("An error occurred while importing '" + projectName + "' project.", e); 37 | throw new RuntimeException(e); 38 | } 39 | 40 | } 41 | 42 | public static Set> listFiles(Path projectPath) { 43 | Set> resources = new HashSet<>(); 44 | Stack stack = new Stack<>(); 45 | File directory = projectPath.toFile(); 46 | 47 | stack.push(directory); 48 | 49 | while (!stack.empty()) { 50 | File current = stack.pop(); 51 | 52 | File[] files = current.listFiles(); 53 | if (files != null) { 54 | for (File file : files) { 55 | if (file.isDirectory()) { 56 | stack.push(file); 57 | } else { 58 | try { 59 | String path = file.getAbsolutePath().replace(projectPath.toFile().getAbsolutePath(), ""); 60 | path = path.substring(1); 61 | path = path.replace("\\", "/"); 62 | resources.add(new AbstractMap.SimpleEntry<>(path, Files.readAllBytes(file.toPath()))); 63 | } catch (IOException e) { 64 | throw new RuntimeException(e); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | return resources; 71 | } 72 | 73 | public static boolean isAnIgnitionResource(String resource) { 74 | return !resource.startsWith(".git") 75 | && !resource.startsWith("tags") 76 | && !resource.startsWith("images") 77 | && !resource.startsWith("themes") 78 | && !resource.equals("project.json"); 79 | } 80 | 81 | public static Set importFromFolder(Path projectPath, String projectName) throws ProjectInvalidException, IOException { 82 | Set resources = new HashSet<>(); 83 | Set createdFolders = new HashSet<>(); 84 | 85 | Set> files = listFiles(projectPath); 86 | files.removeIf(e -> !isAnIgnitionResource(e.getKey())); 87 | files.stream().collect(Collectors.groupingBy(e -> StringUtils.substringBeforeLast(e.getKey(), "/"))) 88 | .forEach((resourcePath, listOfFileNodes) -> { 89 | if (isAnIgnitionResource(resourcePath)) { 90 | StringPath stringPath = StringPath.parse(resourcePath); 91 | resources.addAll(createParentFolderResources(projectName, stringPath, createdFolders)); 92 | String manifestPath = String.format("%s/%s", resourcePath, "resource.json"); 93 | 94 | ProjectResourceManifest resourceManifest = removeResourceManifest(manifestPath, listOfFileNodes); 95 | 96 | if (resourceManifest != null) { 97 | 98 | Map dataMap = createDataMap(resourceManifest, listOfFileNodes); 99 | 100 | resources.add(createResourceBuilder(projectName, stringPath, resourceManifest, dataMap).build()); 101 | } else if (!createdFolders.contains(stringPath)) { 102 | resources.add(createResourceBuilder(projectName, stringPath, ProjectResourceManifest.newBuilder().build(), new HashMap<>()).setFolder(true).build()); 103 | createdFolders.add(stringPath); 104 | } 105 | } 106 | }); 107 | 108 | return resources; 109 | } 110 | 111 | private static ProjectResourceManifest removeResourceManifest(String manifestPath, List> listOfFileNodes) { 112 | return listOfFileNodes.stream() 113 | .filter(e -> manifestPath.equals(e.getKey())) 114 | .findFirst() 115 | .map(entry -> { 116 | listOfFileNodes.remove(entry); 117 | try { 118 | return ProjectResourceManifest.fromJson(new String(entry.getValue(), StandardCharsets.UTF_8)); 119 | } catch (JsonSyntaxException e) { 120 | logger.infof("Malformed resource.json at %s, unable to remove", entry.getKey(), e); 121 | return null; 122 | } 123 | }).orElse(null); 124 | } 125 | 126 | 127 | private static List createParentFolderResources(String projectName, StringPath resourcePath, Set ignoreList) { 128 | List folders = new ArrayList<>(); 129 | StringPath currentPath = resourcePath.getParentPath(); 130 | while (currentPath != null && currentPath.getPathLength() > 0 && 131 | !ignoreList.contains(currentPath)) { 132 | folders.add(createResourceBuilder(projectName, currentPath, 133 | ProjectResourceManifest.newBuilder().build(), new HashMap<>()) 134 | .setFolder(true) 135 | .build()); 136 | ignoreList.add(currentPath); 137 | currentPath = currentPath.getParentPath(); 138 | } 139 | 140 | return folders; 141 | } 142 | 143 | private static Map createDataMap(ProjectResourceManifest resourceManifest, List> listOfFileNodes) { 144 | List allowedFiles = resourceManifest.getFiles(); 145 | HashMap dataMap = new HashMap<>(); 146 | listOfFileNodes.forEach(e -> { 147 | String filename = StringUtils.substringAfterLast(e.getKey(), "/"); 148 | if (allowedFiles.contains(filename)) 149 | dataMap.put(filename, e.getValue()); 150 | }); 151 | return dataMap; 152 | } 153 | 154 | private static ProjectResourceBuilder createResourceBuilder(String projectName, StringPath resourcePath, ProjectResourceManifest manifest, Map dataMap) { 155 | String moduleId = resourcePath.getPathComponent(0); 156 | String resourceType = (resourcePath.getPathLength() > 1) ? resourcePath.getPathComponent(1) : null; 157 | String subPath = (resourcePath.getPathLength() > 2) ? resourcePath.subPath().subPath().toString() : ""; 158 | return ProjectResource.newBuilder() 159 | .setProjectName(projectName) 160 | .setResourcePath(new ResourcePath(new ResourceType(moduleId, resourceType), subPath)) 161 | .setData(dataMap) 162 | .setRestricted(manifest.isRestricted()) 163 | .setAttributes(manifest.getAttributes()) 164 | .setApplicationScope(manifest.getScope()) 165 | .setDocumentation(manifest.getDocumentation()) 166 | .setVersion(manifest.getVersion()) 167 | .setOverridable(manifest.isOverridable()); 168 | } 169 | 170 | public static ProjectManifest loadProjectManifest(Path projectPath) throws IOException { 171 | String json = new String(Files.readAllBytes(projectPath.resolve("project.json")), StandardCharsets.UTF_8); 172 | return ProjectManifest.fromJson(json); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/managers/GitTagManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.inductiveautomation.ignition.common.JsonUtilities; 4 | import com.inductiveautomation.ignition.common.gson.JsonElement; 5 | import com.inductiveautomation.ignition.common.gson.JsonObject; 6 | import com.inductiveautomation.ignition.common.tags.TagUtilities; 7 | import com.inductiveautomation.ignition.common.tags.config.CollisionPolicy; 8 | import com.inductiveautomation.ignition.common.tags.config.TagConfigurationModel; 9 | import com.inductiveautomation.ignition.common.tags.model.TagPath; 10 | import com.inductiveautomation.ignition.common.tags.model.TagProvider; 11 | import com.inductiveautomation.ignition.common.tags.paths.BasicTagPath; 12 | import com.inductiveautomation.ignition.common.tags.paths.parser.TagPathParser; 13 | import com.inductiveautomation.ignition.common.util.LoggerEx; 14 | import com.inductiveautomation.ignition.gateway.tags.model.GatewayTagManager; 15 | import org.apache.commons.io.FileUtils; 16 | import org.apache.commons.io.FilenameUtils; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.nio.charset.StandardCharsets; 21 | import java.nio.file.Files; 22 | import java.nio.file.Path; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.concurrent.CompletableFuture; 26 | 27 | import static com.axone_io.ignition.git.GatewayHook.context; 28 | import static com.axone_io.ignition.git.managers.GitManager.clearDirectory; 29 | import static com.axone_io.ignition.git.managers.GitManager.getProjectFolderPath; 30 | import static com.inductiveautomation.ignition.common.tags.TagUtilities.TAG_GSON; 31 | 32 | public class GitTagManager { 33 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitTagManager.class); 34 | 35 | public static void importTagManager(String projectName) { 36 | Path projectDir = getProjectFolderPath(projectName); 37 | File tagsProjectDir = projectDir.resolve("tags").toFile(); 38 | 39 | GatewayTagManager gatewayTagManager = context.getTagManager(); 40 | if (tagsProjectDir.exists()) { 41 | File[] files = tagsProjectDir.listFiles(); 42 | if (files != null) { 43 | for (File file : files) { 44 | String providerName = FilenameUtils.removeExtension(file.getName()); 45 | TagProvider tagProvider = gatewayTagManager.getTagProvider(providerName); 46 | if (tagProvider != null) { 47 | try { 48 | tagProvider.importTagsAsync(new BasicTagPath(""), FileUtils.readFileToString(file, StandardCharsets.UTF_8.toString()), "JSON", CollisionPolicy.Overwrite, null); 49 | } catch (IOException e) { 50 | logger.warn("An error occurred while importing '" + providerName + "' tags.", e); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | public static void exportTag(Path projectFolderPath) { 59 | Path tagFolderPath = projectFolderPath.resolve("tags"); 60 | clearDirectory(tagFolderPath); 61 | 62 | try { 63 | Files.createDirectories(tagFolderPath); 64 | 65 | for (TagProvider tagProvider : context.getTagManager().getTagProviders()) { 66 | TagPath typesPath = TagPathParser.parse(""); 67 | List tagPaths = new ArrayList<>(); 68 | tagPaths.add(typesPath); 69 | 70 | CompletableFuture> cfTagModels = 71 | tagProvider.getTagConfigsAsync(tagPaths, true, true); 72 | List tModels = cfTagModels.get(); 73 | 74 | JsonObject json = TagUtilities.toJsonObject(tModels.get(0)); 75 | JsonElement sortedJson = JsonUtilities.createDeterministicCopy(json); 76 | 77 | Path newFile = tagFolderPath.resolve(tagProvider.getName() + ".json"); 78 | 79 | Files.writeString(newFile, TAG_GSON.toJson(sortedJson)); 80 | } 81 | } catch (Exception e) { 82 | logger.error(e.toString(), e); 83 | throw new RuntimeException(e); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/managers/GitThemeManager.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.managers; 2 | 3 | import com.inductiveautomation.ignition.common.JsonUtilities; 4 | import com.inductiveautomation.ignition.common.gson.Gson; 5 | import com.inductiveautomation.ignition.common.gson.JsonObject; 6 | import com.inductiveautomation.ignition.common.util.LoggerEx; 7 | import org.apache.commons.io.FileUtils; 8 | import org.apache.commons.io.FilenameUtils; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.StandardCopyOption; 15 | 16 | import static com.axone_io.ignition.git.managers.GitManager.*; 17 | 18 | public class GitThemeManager { 19 | private final static LoggerEx logger = LoggerEx.newBuilder().build(GitThemeManager.class); 20 | 21 | public static void importTheme(String projectName) { 22 | Path dataDir = getDataFolderPath(); 23 | Path projectDir = getProjectFolderPath(projectName); 24 | Path themesDir = dataDir.resolve("modules").resolve("com.inductiveautomation.perspective").resolve("themes"); 25 | Path themesProjectDir = projectDir.resolve("themes"); 26 | File themesProjectDirFile = themesProjectDir.toFile(); 27 | 28 | if (themesProjectDirFile.exists()) { 29 | File[] files = themesProjectDirFile.listFiles(); 30 | if (files != null) { 31 | for (File file : files) { 32 | if (file.isFile()) { 33 | String themeName = FilenameUtils.removeExtension(file.getName()); 34 | try { 35 | Path destinationDirectoryMain = themesDir.resolve(file.getName()); 36 | Path sourceDirectoryMain = themesProjectDir.resolve(file.getName()); 37 | Files.copy(sourceDirectoryMain, destinationDirectoryMain, StandardCopyOption.REPLACE_EXISTING); 38 | 39 | File destinationDirectory = themesDir.resolve(themeName).toFile(); 40 | File sourceDirectory = themesProjectDir.resolve(themeName).toFile(); 41 | FileUtils.deleteDirectory(destinationDirectory); 42 | FileUtils.copyDirectory(sourceDirectory, destinationDirectory); 43 | } catch (IOException e) { 44 | logger.warn("An error occurred while importing '" + themeName + "' theme.", e); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | public static void exportTheme(Path projectFolderPath) { 53 | try { 54 | Path sessionPropsPath = projectFolderPath.resolve("com.inductiveautomation.perspective") 55 | .resolve("session-props") 56 | .resolve("props.json"); 57 | String content = Files.readString(sessionPropsPath); 58 | Gson g = new Gson(); 59 | JsonObject json = g.fromJson(content, JsonObject.class); 60 | String theme = JsonUtilities.readString(json, "props.theme", "light"); 61 | 62 | Path themesDir = getDataFolderPath() 63 | .resolve("modules") 64 | .resolve("com.inductiveautomation.perspective") 65 | .resolve("themes"); 66 | 67 | Path themeFolder = themesDir.resolve(theme); 68 | Path themeFile = themesDir.resolve(theme + ".css"); 69 | 70 | Path themeFolderPath = projectFolderPath.resolve("themes"); 71 | clearDirectory(themeFolderPath); 72 | Files.createDirectories(themeFolderPath); 73 | FileUtils.copyDirectoryToDirectory(themeFolder.toFile(), themeFolderPath.toFile()); 74 | Files.copy(themeFile, themeFolderPath.resolve(themeFile.getFileName())); 75 | } catch (IOException e) { 76 | logger.error(e.toString(), e); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/records/GitProjectsConfigRecord.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.records; 2 | 3 | import com.axone_io.ignition.git.web.ProjectList.ProjectListEditorSource; 4 | import com.inductiveautomation.ignition.gateway.localdb.persistence.*; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import simpleorm.dataset.SFieldFlags; 8 | 9 | public class GitProjectsConfigRecord extends PersistentRecord { 10 | private static final Logger logger = LoggerFactory.getLogger(GitProjectsConfigRecord.class); 11 | 12 | public static final RecordMeta META = new RecordMeta<>( 13 | GitProjectsConfigRecord.class, "GitProjectsConfigRecord"); 14 | 15 | @Override 16 | public RecordMeta getMeta() { 17 | return META; 18 | } 19 | 20 | public static final IdentityField Id = new IdentityField(META); 21 | public static final StringField ProjectName = new StringField(META, "ProjectName", SFieldFlags.SMANDATORY, SFieldFlags.SDESCRIPTIVE); 22 | public static final StringField URI = 23 | new StringField(META, "URI", SFieldFlags.SMANDATORY, SFieldFlags.SDESCRIPTIVE); 24 | 25 | 26 | static final Category ProjectConfiguration = new Category("GitProjectsConfigRecord.Category.ProjectConfiguration", 1000).include(ProjectName, URI); 27 | 28 | 29 | public long getId() { 30 | return this.getLong(Id); 31 | } 32 | 33 | public String getProjectName() { 34 | return this.getString(ProjectName); 35 | } 36 | 37 | public String getURI() { 38 | return this.getString(URI); 39 | } 40 | 41 | public void setProjectName(String projectName) { 42 | setString(ProjectName, projectName); 43 | } 44 | 45 | public void setURI(String uri) { 46 | setString(URI, uri); 47 | } 48 | 49 | public boolean isSSHAuthentication() { 50 | return !this.getString(URI).toLowerCase().startsWith("http"); 51 | } 52 | 53 | static { 54 | ProjectName.getFormMeta().setEditorSource(ProjectListEditorSource.getSharedInstance()); 55 | 56 | URI.getFormMeta().setFieldDescriptionKey("GitProjectsConfigRecord.URI.Desc"); 57 | URI.getFormMeta().setFieldDescriptionKeyAddMode("GitProjectsConfigRecord.URI.NewDesc"); 58 | URI.getFormMeta().setFieldDescriptionKeyEditMode("GitProjectsConfigRecord.URI.EditDesc"); 59 | URI.setWide(); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/records/GitReposUsersRecord.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.records; 2 | 3 | import com.inductiveautomation.ignition.gateway.localdb.persistence.*; 4 | import com.inductiveautomation.ignition.gateway.web.components.editors.PasswordEditorSource; 5 | import com.inductiveautomation.ignition.gateway.web.components.editors.TextAreaEditorSource; 6 | import org.apache.wicket.validation.validator.EmailAddressValidator; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import simpleorm.dataset.SFieldFlags; 10 | 11 | public class GitReposUsersRecord extends PersistentRecord { 12 | private static final Logger logger = LoggerFactory.getLogger(GitReposUsersRecord.class); 13 | 14 | public static final RecordMeta META = new RecordMeta<>( 15 | GitReposUsersRecord.class, "GitReposUsersRecord"); 16 | 17 | @Override 18 | public RecordMeta getMeta() { 19 | return META; 20 | } 21 | 22 | public static final IdentityField Id = new IdentityField(META); 23 | public static final LongField ProjectId = new LongField(META, "ProjectId"); 24 | public static final ReferenceField ProjectName = new ReferenceField<>(META, GitProjectsConfigRecord.META, "ProjectName", ProjectId); 25 | public static final ReferenceField URI = new ReferenceField<>(META, GitProjectsConfigRecord.META, "URI", ProjectId); 26 | 27 | public static final StringField IgnitionUser = new StringField(META, "IgnitionUser", SFieldFlags.SPRIMARY_KEY, SFieldFlags.SMANDATORY, SFieldFlags.SDESCRIPTIVE); 28 | public static final StringField SSHKey = new StringField(META, "SSHKey"); 29 | public static final StringField UserName = new StringField(META, "UserName"); 30 | public static final StringField Email = new StringField(META, "Email", SFieldFlags.SMANDATORY, SFieldFlags.SDESCRIPTIVE).setDefault(""); 31 | public static final EncodedStringField Password = new EncodedStringField(META, "Password"); 32 | 33 | static final Category UserProperties = new Category("GitReposUsersRecord.Category.UserProperties", 1000).include(ProjectName, IgnitionUser, UserName, Email, SSHKey, Password); 34 | 35 | public int getId() { 36 | return this.getInt(Id); 37 | } 38 | 39 | public int getProjectId() { 40 | return this.getInt(ProjectId); 41 | } 42 | 43 | public String getUserName() { 44 | return this.getString(UserName); 45 | } 46 | 47 | public String getEmail() { 48 | return this.getString(Email); 49 | } 50 | 51 | public String getIgnitionUser() { 52 | return this.getString(IgnitionUser); 53 | } 54 | 55 | public String getProjectName() { 56 | return this.getString(ProjectName); 57 | } 58 | 59 | public String getPassword() { 60 | return this.getString(Password); 61 | } 62 | 63 | public String getSSHKey() { 64 | return this.getString(SSHKey); 65 | } 66 | 67 | public void setUserName(String userName) { 68 | setString(UserName, userName); 69 | } 70 | 71 | public void setPassword(String password) { 72 | setString(Password, password); 73 | } 74 | 75 | public void setIgnitionUser(String ignitionUser) { 76 | setString(IgnitionUser, ignitionUser); 77 | } 78 | 79 | public void setSSHKey(String sshKey) { 80 | setString(SSHKey, sshKey); 81 | } 82 | 83 | public void setEmail(String email) { 84 | setString(Email, email); 85 | } 86 | 87 | public void setProjectId(long projectId) { 88 | this.setLong(ProjectId, projectId); 89 | } 90 | 91 | static { 92 | ProjectName.getFormMeta().setEnabled(false); 93 | URI.getFormMeta().setVisible(false); 94 | 95 | IgnitionUser.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.IgnitionUser.Desc"); 96 | IgnitionUser.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.IgnitionUser.NewDesc"); 97 | IgnitionUser.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.IgnitionUser.EditDesc"); 98 | 99 | ProjectName.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.ProjectName.Desc"); 100 | ProjectName.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.ProjectName.NewDesc"); 101 | ProjectName.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.ProjectName.EditDesc"); 102 | 103 | SSHKey.getFormMeta().setEditorSource(new TextAreaEditorSource()); 104 | SSHKey.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.SSHKey.Desc"); 105 | SSHKey.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.SSHKey.NewDesc"); 106 | SSHKey.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.SSHKey.EditDesc"); 107 | SSHKey.setWide(); 108 | 109 | UserName.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.UserName.Desc"); 110 | UserName.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.UserName.NewDesc"); 111 | UserName.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.UserName.EditDesc"); 112 | 113 | Email.getFormMeta().addValidator(EmailAddressValidator.getInstance()); 114 | Email.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.Email.Desc"); 115 | Email.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.Email.NewDesc"); 116 | Email.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.Email.EditDesc"); 117 | 118 | Password.getFormMeta().setFieldDescriptionKey("GitReposUsersRecord.Password.Desc"); 119 | Password.getFormMeta().setFieldDescriptionKeyAddMode("GitReposUsersRecord.Password.NewDesc"); 120 | Password.getFormMeta().setFieldDescriptionKeyEditMode("GitReposUsersRecord.Password.EditDesc"); 121 | Password.getFormMeta().setEditorSource(PasswordEditorSource.getSharedInstance()); 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/GitProjectsConfigEditPage.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web; 2 | 3 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 4 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditForm; 5 | import com.inductiveautomation.ignition.gateway.web.models.CompoundRecordModel; 6 | import com.inductiveautomation.ignition.gateway.web.models.RecordTypeNameModel; 7 | import com.inductiveautomation.ignition.gateway.web.pages.IConfigPage; 8 | 9 | public class GitProjectsConfigEditPage extends RecordEditForm { 10 | public GitProjectsConfigEditPage(IConfigPage configPage, GitProjectsConfigPage returnPanel, GitProjectsConfigRecord record) { 11 | super(configPage, returnPanel, new RecordTypeNameModel(GitProjectsConfigRecord.META, "RecordActionTable." + ( 12 | record.isNewRow() ? "New" : "Edit") + "RecordAction.PanelTitle"), new CompoundRecordModel(new GitProjectsConfigRecord[] { record })); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/GitProjectsConfigPage.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web; 2 | 3 | import com.axone_io.ignition.git.GatewayHook; 4 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 5 | import com.inductiveautomation.ignition.gateway.localdb.persistence.RecordMeta; 6 | import com.inductiveautomation.ignition.gateway.web.components.ConfigPanel; 7 | import com.inductiveautomation.ignition.gateway.web.components.RecordActionTable; 8 | import com.inductiveautomation.ignition.gateway.web.components.actions.AbstractRecordInstanceAction; 9 | import com.inductiveautomation.ignition.gateway.web.components.actions.EditRecordAction; 10 | import com.inductiveautomation.ignition.gateway.web.components.actions.NewRecordAction; 11 | import com.inductiveautomation.ignition.gateway.web.models.DefaultConfigTab; 12 | import com.inductiveautomation.ignition.gateway.web.models.IConfigTab; 13 | import com.inductiveautomation.ignition.gateway.web.models.LenientResourceModel; 14 | import com.inductiveautomation.ignition.gateway.web.models.RecordTypeNameModel; 15 | import com.inductiveautomation.ignition.gateway.web.pages.IConfigPage; 16 | import org.apache.commons.lang3.tuple.Pair; 17 | import org.apache.wicket.markup.html.WebMarkupContainer; 18 | import org.apache.wicket.markup.repeater.RepeatingView; 19 | import org.apache.wicket.model.IModel; 20 | 21 | import static com.axone_io.ignition.git.GatewayHook.CONFIG_CATEGORY; 22 | 23 | 24 | public class GitProjectsConfigPage extends RecordActionTable { 25 | public static final IConfigTab MENU_ENTRY = DefaultConfigTab.builder() 26 | .category(CONFIG_CATEGORY) 27 | .name("projects") 28 | .i18n("bundle_git.Config.Git.Projects.MenuTitle") 29 | .page(GitProjectsConfigPage.class) 30 | .terms(new String[] { "git", "projects", "users", "ssh"}) 31 | .build(); 32 | 33 | 34 | public GitProjectsConfigPage(IConfigPage configPage) { 35 | super(configPage); 36 | } 37 | @Override 38 | protected RecordMeta getRecordMeta() { 39 | return GitProjectsConfigRecord.META; 40 | } 41 | 42 | protected WebMarkupContainer newRecordAction(String id) { 43 | return new NewRecordAction<>(id, this.configPage, this, GitProjectsConfigRecord.META) { 44 | private static final long serialVersionUID = 1L; 45 | 46 | protected ConfigPanel newRecordEditPanel(GitProjectsConfigRecord newRecord) { 47 | return new GitProjectsConfigEditPage(getConfigPage(), GitProjectsConfigPage.this, newRecord); 48 | } 49 | 50 | protected void setupNewRecord(GitProjectsConfigRecord record) { 51 | } 52 | }; 53 | } 54 | 55 | protected WebMarkupContainer newEditRecordAction(String id, GitProjectsConfigRecord record) { 56 | if (record.getId() == -1L) 57 | return null; 58 | return new EditRecordAction<>(id, this.configPage, this, record) { 59 | private static final long serialVersionUID = 1L; 60 | 61 | protected ConfigPanel createPanel(GitProjectsConfigRecord record) { 62 | return new GitProjectsConfigEditPage(getConfigPage(), GitProjectsConfigPage.this, record); 63 | } 64 | }; 65 | } 66 | 67 | @Override 68 | protected void addRecordInstanceActions (RepeatingView view, GitProjectsConfigRecord main) { 69 | super.addRecordInstanceActions(view, main); 70 | view.add(new ManageAction(view.newChildId(), this.configPage, this, main)); 71 | } 72 | 73 | @Override 74 | public Pair getMenuLocation() { 75 | return MENU_ENTRY.getMenuLocation(); 76 | } 77 | 78 | private static class ManageAction extends AbstractRecordInstanceAction { 79 | public ManageAction(String id, IConfigPage configPage, ConfigPanel parentPanel, GitProjectsConfigRecord record) { 80 | super(id, configPage, parentPanel, record); 81 | } 82 | 83 | protected ConfigPanel createPanel(GitProjectsConfigRecord record) { 84 | return new GitReposUsersPage(getConfigPage(), record); 85 | } 86 | 87 | protected String getCssClass() { 88 | return "view"; 89 | } 90 | 91 | public IModel getLabel() { 92 | return new RecordTypeNameModel(GitProjectsConfigRecord.META, "Config.Projects.UserLink"); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/GitReposUsersEditPage.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web; 2 | 3 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 4 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 5 | import com.inductiveautomation.ignition.gateway.model.GatewayContext; 6 | import com.inductiveautomation.ignition.gateway.model.IgnitionWebApp; 7 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditForm; 8 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditMode; 9 | import com.inductiveautomation.ignition.gateway.web.models.CompoundRecordModel; 10 | import com.inductiveautomation.ignition.gateway.web.models.RecordTypeNameModel; 11 | import com.inductiveautomation.ignition.gateway.web.pages.IConfigPage; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.apache.wicket.Application; 14 | import org.apache.wicket.validation.IValidatable; 15 | import org.apache.wicket.validation.IValidator; 16 | import org.apache.wicket.validation.ValidationError; 17 | import simpleorm.dataset.SQuery; 18 | 19 | import static com.axone_io.ignition.git.records.GitReposUsersRecord.IgnitionUser; 20 | import static com.axone_io.ignition.git.records.GitReposUsersRecord.ProjectId; 21 | 22 | public class GitReposUsersEditPage extends RecordEditForm { 23 | protected RecordEditMode mode; 24 | 25 | public GitReposUsersEditPage(IConfigPage configPage, GitReposUsersPage returnPanel, GitReposUsersRecord record) { 26 | super(configPage, returnPanel, new RecordTypeNameModel(GitReposUsersRecord.META, "RecordActionTable." + ( 27 | record.isNewRow() ? "New" : "Edit") + "RecordAction.PanelTitle"), new CompoundRecordModel(new GitReposUsersRecord[]{record})); 28 | 29 | mode = record.isNewRow() ? RecordEditMode.ADD : RecordEditMode.EDIT; 30 | // IgnitionUser.addValidator(new UniqueUsernameValidator()); 31 | } 32 | 33 | protected String getIgnitionUser() { 34 | return ((GitReposUsersRecord) getDefaultModelObject()).getIgnitionUser(); 35 | } 36 | protected int getProjectId() { 37 | return ((GitReposUsersRecord) getDefaultModelObject()).getProjectId(); 38 | } 39 | 40 | private class UniqueUsernameValidator implements IValidator { 41 | private RecordEditMode mode; 42 | private String original; 43 | 44 | private int projectId; 45 | 46 | public UniqueUsernameValidator() { 47 | this.mode = GitReposUsersEditPage.this.mode; 48 | this.original = GitReposUsersEditPage.this.getIgnitionUser(); 49 | 50 | this.projectId = GitReposUsersEditPage.this.getProjectId(); 51 | } 52 | 53 | protected boolean isUnique(GatewayContext context, String value) { 54 | SQuery query = new SQuery<>(GitReposUsersRecord.META).eq(ProjectId, projectId).eq(IgnitionUser, value); 55 | 56 | GitReposUsersRecord result = context.getPersistenceInterface().queryOne(query); 57 | return result == null; 58 | } 59 | 60 | public void validate(IValidatable validatable) { 61 | if (this.isEnabled()) { 62 | String attempt = validatable.getValue(); 63 | if (!StringUtils.isBlank(attempt)) { 64 | if (this.mode != RecordEditMode.EDIT || !StringUtils.equalsIgnoreCase(attempt, this.original)) { 65 | if (!this.isUnique(((IgnitionWebApp) Application.get()).getContext(), attempt)) { 66 | validatable.error((new ValidationError()).addKey("GitReposUsersRecord.IgnitionUser.FieldEditor.Unique")); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | protected boolean isEnabled() { 74 | return true; 75 | } 76 | 77 | 78 | } 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/GitReposUsersPage.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web; 2 | 3 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 4 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 5 | import com.axone_io.ignition.git.web.component.CustomRecordListModel; 6 | import com.inductiveautomation.ignition.gateway.localdb.persistence.RecordMeta; 7 | import com.inductiveautomation.ignition.gateway.web.components.ConfigPanel; 8 | import com.inductiveautomation.ignition.gateway.web.components.RecordActionTable; 9 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditMode; 10 | import com.inductiveautomation.ignition.gateway.web.components.actions.EditRecordAction; 11 | import com.inductiveautomation.ignition.gateway.web.components.actions.NewRecordAction; 12 | import com.inductiveautomation.ignition.gateway.web.pages.IConfigPage; 13 | import org.apache.commons.lang3.tuple.Pair; 14 | import org.apache.wicket.markup.html.WebMarkupContainer; 15 | import simpleorm.dataset.SFieldFlags; 16 | 17 | import static com.axone_io.ignition.git.web.GitProjectsConfigPage.MENU_ENTRY; 18 | 19 | 20 | public class GitReposUsersPage extends RecordActionTable { 21 | GitProjectsConfigRecord gitProjectsConfigRecord; 22 | 23 | public GitReposUsersPage(IConfigPage configPage, GitProjectsConfigRecord record) { 24 | super(configPage, new CustomRecordListModel(GitReposUsersRecord.META, record.getId())); 25 | this.gitProjectsConfigRecord = record; 26 | } 27 | 28 | @Override 29 | protected RecordMeta getRecordMeta() { 30 | return GitReposUsersRecord.META; 31 | } 32 | 33 | protected WebMarkupContainer newRecordAction(String id) { 34 | return new NewRecordAction<>(id, this.configPage, this, GitReposUsersRecord.META) { 35 | private static final long serialVersionUID = 1L; 36 | 37 | protected ConfigPanel newRecordEditPanel(GitReposUsersRecord newRecord) { 38 | if (gitProjectsConfigRecord != null) newRecord.setProjectId(gitProjectsConfigRecord.getId()); 39 | setupPanel(gitProjectsConfigRecord, newRecord, RecordEditMode.ADD); 40 | return new GitReposUsersEditPage(getConfigPage(), new GitReposUsersPage(configPage, gitProjectsConfigRecord), newRecord); 41 | } 42 | 43 | protected void setupNewRecord(GitReposUsersRecord newRecord) { 44 | } 45 | }; 46 | } 47 | 48 | protected WebMarkupContainer newEditRecordAction(String id, GitReposUsersRecord record) { 49 | if (record.getId() == -1L) 50 | return null; 51 | return new EditRecordAction<>(id, this.configPage, this, record) { 52 | private static final long serialVersionUID = 1L; 53 | 54 | protected ConfigPanel createPanel(GitReposUsersRecord record) { 55 | setupPanel(gitProjectsConfigRecord, record, RecordEditMode.EDIT); 56 | return new GitReposUsersEditPage(getConfigPage(), new GitReposUsersPage(configPage, gitProjectsConfigRecord), record); 57 | } 58 | }; 59 | 60 | } 61 | 62 | @Override 63 | public Pair getMenuLocation() { 64 | return MENU_ENTRY.getMenuLocation(); 65 | } 66 | 67 | void setupPanel(GitProjectsConfigRecord projectRecord, GitReposUsersRecord record, RecordEditMode mode) { 68 | boolean sshAuth = projectRecord.isSSHAuthentication(); 69 | GitReposUsersRecord.SSHKey.getFormMeta().setVisible(sshAuth); 70 | GitReposUsersRecord.UserName.getFormMeta().setVisible(!sshAuth); 71 | if (!sshAuth) { 72 | GitReposUsersRecord.UserName.addFlag(SFieldFlags.SMANDATORY); 73 | } 74 | GitReposUsersRecord.Password.getFormMeta().setVisible(!sshAuth); 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/ProjectList/ProjectListEditorSource.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web.ProjectList; 2 | 3 | import com.inductiveautomation.ignition.gateway.localdb.persistence.FormMeta; 4 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditMode; 5 | import com.inductiveautomation.ignition.gateway.web.components.editors.IEditorSource; 6 | import org.apache.wicket.Component; 7 | import simpleorm.dataset.SRecordInstance; 8 | 9 | /** 10 | * IEditorSource implementation for project source list. 11 | * @see IEditorSource 12 | */ 13 | public class ProjectListEditorSource implements IEditorSource{ 14 | /** 15 | * Shared constant instance of this ProjectListEditorSource. 16 | */ 17 | static final ProjectListEditorSource _instance = new ProjectListEditorSource(); 18 | 19 | /** 20 | * Get the shared instance of this ProjectListEditorSource. 21 | * @return shared instance 22 | */ 23 | public static ProjectListEditorSource getSharedInstance() { 24 | return _instance; 25 | } 26 | 27 | /** 28 | * Default constructor for ProjectListEditorSource. Performs no operations. 29 | */ 30 | public ProjectListEditorSource() { 31 | 32 | } 33 | 34 | /** 35 | * Creates a new ProjectSourceEditorSource with given information. 36 | * @param id identifier 37 | * @param editMode edit mode 38 | * @param record record instance 39 | * @param formMeta form meta 40 | * @return created ProjectSourceEditorSource 41 | */ 42 | @Override 43 | public Component newEditorComponent(String id, RecordEditMode editMode, SRecordInstance record, 44 | FormMeta formMeta) { 45 | return new ProjectSourceEditor(id, formMeta, editMode, record); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/ProjectList/ProjectSourceEditor.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web.ProjectList; 2 | 3 | import com.axone_io.ignition.git.records.GitProjectsConfigRecord; 4 | import com.inductiveautomation.ignition.gateway.localdb.persistence.FormMeta; 5 | import com.inductiveautomation.ignition.gateway.model.GatewayContext; 6 | import com.inductiveautomation.ignition.gateway.model.IgnitionWebApp; 7 | import com.inductiveautomation.ignition.gateway.web.components.RecordEditMode; 8 | import com.inductiveautomation.ignition.gateway.web.components.editors.AbstractEditor; 9 | import com.inductiveautomation.ignition.gateway.web.models.IRecordFieldComponent; 10 | import com.inductiveautomation.ignition.gateway.web.models.LenientResourceModel; 11 | import org.apache.wicket.Application; 12 | import org.apache.wicket.markup.html.form.DropDownChoice; 13 | import simpleorm.dataset.SFieldMeta; 14 | import simpleorm.dataset.SQuery; 15 | import simpleorm.dataset.SRecordInstance; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Project source editor component 22 | */ 23 | public class ProjectSourceEditor extends AbstractEditor { 24 | 25 | /** 26 | * Create a new project source editor with given information 27 | * @param id identifier 28 | * @param formMeta form meta 29 | * @param editMode edit mode 30 | * @param record record instance 31 | */ 32 | @SuppressWarnings("unchecked") 33 | public ProjectSourceEditor(String id, FormMeta formMeta, RecordEditMode editMode, 34 | SRecordInstance record) { 35 | super(id, formMeta, editMode, record); 36 | 37 | ProjectDropdownChoice dropdown = new ProjectDropdownChoice("editor", record, editMode); 38 | 39 | formMeta.installValidators(dropdown); 40 | 41 | dropdown.setLabel(new LenientResourceModel(formMeta.getFieldNameKey())); 42 | 43 | add(dropdown); 44 | 45 | } 46 | 47 | /** 48 | * Dropdown chooser component for project selection. 49 | */ 50 | private class ProjectDropdownChoice extends DropDownChoice 51 | implements IRecordFieldComponent { 52 | 53 | /** 54 | * Create a new dropdown chooser component for project selection. 55 | * @param id identifier 56 | * @param record record instance 57 | */ 58 | @SuppressWarnings("unchecked") 59 | public ProjectDropdownChoice(String id, SRecordInstance record, RecordEditMode editMode) { 60 | super(id); 61 | 62 | GatewayContext context = ((IgnitionWebApp) Application.get()).getContext(); 63 | 64 | // We manage the list of available projects. 65 | List stores = context.getProjectManager().getProjectNames(); 66 | SQuery query = new SQuery<>(GitProjectsConfigRecord.META); 67 | List results = context.getPersistenceInterface().query(query); 68 | GitProjectsConfigRecord gitRecord = (GitProjectsConfigRecord) record; 69 | 70 | // We delete the projects already used to avoid duplication. 71 | // The name of the selected project is not deleted in edit mode so that the selected project can be edited without being modified. 72 | for(GitProjectsConfigRecord p: results){ 73 | if (!(editMode == RecordEditMode.EDIT && p.getProjectName().equals(gitRecord.getProjectName()))){ 74 | stores.remove(p.getProjectName()); 75 | } 76 | } 77 | 78 | setChoices(stores); 79 | } 80 | 81 | /** 82 | * Get meta for dropdown chooser field. 83 | * @return dropdown chooser field meta 84 | */ 85 | public SFieldMeta getFieldMeta() { 86 | return getFormMeta().getField(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /git-gateway/src/main/java/com/axone_io/ignition/git/web/component/CustomRecordListModel.java: -------------------------------------------------------------------------------- 1 | package com.axone_io.ignition.git.web.component; 2 | 3 | import com.axone_io.ignition.git.records.GitReposUsersRecord; 4 | import com.inductiveautomation.ignition.gateway.localdb.persistence.PersistentRecord; 5 | import com.inductiveautomation.ignition.gateway.localdb.persistence.RecordMeta; 6 | import com.inductiveautomation.ignition.gateway.web.models.RecordListModel; 7 | 8 | public class CustomRecordListModel extends RecordListModel { 9 | long value; 10 | public CustomRecordListModel(RecordMeta meta, long value) { 11 | super(meta); 12 | this.value = value; 13 | } 14 | 15 | // TODO : Find a way to deport GitReposUsersRecord.ProjectId in the constructor parameters 16 | // If a LongField is added in the parameters of the constructor, a NullPointerException is thrown during a "backward to last page" from the web browser. 17 | @Override 18 | protected boolean filter(PersistentRecord record) { 19 | return record.getLong(GitReposUsersRecord.ProjectId) != value; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /git-gateway/src/main/resources/com/axone_io/ignition/git/bundle_git.properties: -------------------------------------------------------------------------------- 1 | module.Name=Git 2 | 3 | Config.Git.MenuTitle=Git 4 | Config.Git.Projects.MenuTitle=Projects 5 | 6 | -------------------------------------------------------------------------------- /git-gateway/src/main/resources/com/axone_io/ignition/git/records/GitProjectsConfigRecord.properties: -------------------------------------------------------------------------------- 1 | ProjectName.Name=Project Name 2 | ProjectName.Desc=The name of Ignition project sibling. 3 | ProjectName.NewDesc=The name of Ignition project sibling. 4 | ProjectName.EditDesc=The name of Ignition project sibling. 5 | 6 | URI.Name=URI 7 | URI.Desc=The URI of git repository. Http or SSH. 8 | URI.NewDesc=The URI of git repository. Http or SSH. 9 | URI.EditDesc=The URI of git repository. Http or SSH. 10 | 11 | Category.ProjectConfiguration=Project Configuration 12 | 13 | Noun=Project 14 | NounPlural=Projects 15 | -------------------------------------------------------------------------------- /git-gateway/src/main/resources/com/axone_io/ignition/git/records/GitReposUsersRecord.properties: -------------------------------------------------------------------------------- 1 | IgnitionUser.Name=Ignition User 2 | IgnitionUser.Desc=User in project user source. (ex: admin) 3 | IgnitionUser.NewDesc=User in project user source. (ex: admin) 4 | IgnitionUser.EditDesc=User in project user source. (ex: admin) 5 | IgnitionUser.FieldEditor.Unique=${label} "${input}" is already declared in this project. 6 | 7 | 8 | ProjectName.Name=Project Name 9 | ProjectName.Desc=The name of Ignition project sibling. 10 | ProjectName.NewDesc=The name of Ignition project sibling. 11 | ProjectName.EditDesc=The name of Ignition project sibling. 12 | 13 | SSHKey.Name=SSH Key 14 | SSHKey.Desc=The SSH Key of your user git account. 15 | SSHKey.NewDesc=The SSH Key of your user git account. 16 | SSHKey.EditDesc=The SSH Key of your user git account. 17 | 18 | UserName.Name=Username 19 | UserName.Desc=The username of the git account. 20 | UserName.NewDesc=The username of the git account. 21 | UserName.EditDesc=The username of the git account. 22 | 23 | Email.Name=Email 24 | Email.Desc=The email of the git account. 25 | Email.NewDesc=The email of the git account. 26 | Email.EditDesc=The email of the git account. 27 | 28 | Password.Name=Password 29 | Password.Desc=The password or personal access token for the git account. 30 | Password.NewDesc=The password or personal access token for the git account. 31 | Password.EditDesc=The password or personal access token for the git account. 32 | 33 | Category.UserProperties=User Properties 34 | 35 | RecordActionTable.NewRecordAction.Link=Create User 36 | RecordActionTable.NewRecordAction.PanelTitle=New User 37 | RecordActionTable.EditRecordAction.PanelTitle=Edit User 38 | 39 | 40 | Noun=User 41 | NounPlural=Users 42 | -------------------------------------------------------------------------------- /git-gateway/src/main/resources/com/axone_io/ignition/git/web/GitProjectsConfigPage.properties: -------------------------------------------------------------------------------- 1 | Config.Projects.UserLink=manage users 2 | -------------------------------------------------------------------------------- /git-gateway/src/main/resources/com/axone_io/ignition/git/web/ProjectList/ProjectSourceEditor.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /img/CommitPopup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AXONE-IO/ignition-git-module/6e3f3cb4135ba47223439acad23ad780363f02df/img/CommitPopup.png -------------------------------------------------------------------------------- /img/GitStatusBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AXONE-IO/ignition-git-module/6e3f3cb4135ba47223439acad23ad780363f02df/img/GitStatusBar.png -------------------------------------------------------------------------------- /img/GitToolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AXONE-IO/ignition-git-module/6e3f3cb4135ba47223439acad23ad780363f02df/img/GitToolbar.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.axone_io.ignition 8 | git 9 | 1.0.2 10 | pom 11 | 12 | 13 | git-build 14 | git-client 15 | git-common 16 | git-designer 17 | git-gateway 18 | 19 | 20 | 21 | 8.1.0 22 | ${ignition-platform-version} 23 | 11 24 | Git 25 | Adds a simple scripting function to the client and gateway 26 | UTF-8 27 | UTF-8 28 | 29 | 30 | 31 | 32 | releases 33 | https://nexus.inductiveautomation.com/repository/inductiveautomation-releases 34 | 35 | true 36 | always 37 | 38 | 39 | false 40 | 41 | 42 | 43 | 44 | 45 | 46 | releases 47 | https://nexus.inductiveautomation.com/repository/inductiveautomation-releases 48 | 49 | false 50 | 51 | 52 | true 53 | always 54 | 55 | 56 | 57 | 58 | snapshots 59 | https://nexus.inductiveautomation.com/repository/inductiveautomation-snapshots 60 | 61 | true 62 | always 63 | 64 | 65 | false 66 | 67 | 68 | 69 | 70 | thirdparty 71 | https://nexus.inductiveautomation.com/repository/inductiveautomation-thirdparty 72 | 73 | true 74 | always 75 | 76 | 77 | false 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ignition Git Module 2 | 3 | [![License](https://img.shields.io/badge/license-Beerware-green.svg)](LICENSE.md) 4 | 5 | Integrated git client in free Ignition module. 6 | 7 | ## Presentation 8 | 9 | The Git module is an Ignition module embedding a Git client to make its integration easier into Ignition project development.
10 | It permits to manage project resources throughout the development process in the Ignition designer.
11 | Exporting gateway configuration is simplified or even automated. 12 | 13 | ## Features 14 | 15 | - Link an Ignition project with a remote repo, (Gateway Webpage) 16 | - Link an Ignition user to a git project, with ssh or user/password authentication, (Gateway Webpage) 17 | - Commit resources, (Designer, on project saved or from git toolbar) 18 | - Push & Pull resources, (Designer, from git toolbar) 19 | - Export of the gateway configuration. Tags, images, theme... (Designer, from git toolbar) 20 | - Commit popup :
21 | ![Commit Popup](./img/CommitPopup.png) 22 | - Toolbar :
23 | ![Git Toolbar](./img/GitToolbar.png) 24 | - Status Bar :
25 | ![Git Toolbar](./img/GitStatusBar.png) 26 | - Commissioning configuration file for easy deployment.
27 | 28 | ## Module Documentation 29 | You can find the documentation of the module [HERE](https://www.axone-io.com/Files/Modules/GIT/1.0.2/doc/index.html), depending on its version. 30 | 31 | You will also find a download link to the signed version of the module. 32 | 33 | ## Installation for development 34 | ### Prerequisites 35 | 36 | Before installing and running this project on your local machine, make sure you have installed the following : 37 | 38 | - Java (JDK >= 11) 39 | - Maven 40 | - Java IDE (I recommend [Intellij](https://www.jetbrains.com/idea/download/)) 41 | 42 | If you are using Intellij, Maven is already integrated in the IDE and you can easily download the right Java SDK from your project settings. 43 | 44 | ### Installation Instructions 45 | 46 | To install and run this project on your local machine, follow these steps : 47 | 48 | 1. Clone the repo to your local machine: `git clone https://github.com/your-username/your-project.git`. 49 | 2. Open the project in your preferred IDE. 50 | 3. Build the project using Maven by running the following command: mvn clean package. 51 | 4. Install the module on your gateway. 52 | 53 | That's it ! You're ready to start working with the project on your local machine. 54 | 55 | ## Roadmap 56 | 57 | - Branch management, 58 | - Project options for select which resources export on ExportGatewayConfig : 59 | - Tags, which tag provider, which folder… 60 | - Timestamp changes in commit popup, 61 | - Status page : 62 | - List commit, 63 | - Repos state. 64 | - SideDesignerBar for commit management like VisualStudioCode, 65 | - Find a way to find out who deleted the resources, 66 | - Vision project management : 67 | - Auto export bin file to xml. 68 | - Make it impossible to create the same ignition user twice for the same project. 69 | 70 | ## Contributing 71 | 72 | We're thrilled that you want to contribute to this project !
73 | Here are a few steps to get started : 74 | - Fork the repo and clone your fork to your local machine. 75 | - Create a branch for your feature : git checkout -b feature/describe-your-feature. 76 | - Make your changes or add the new feature. 77 | - Commit your changes, clearly explaining what you did : git commit -m "Added a new feature: describe your feature". 78 | - Push your changes to your fork : git push origin feature/describe-your-feature. 79 | - Open a pull request, explaining the changes you made and why they should be included in the project. 80 | 81 | We'll review your contribution as soon as possible and provide feedback.
82 | Thanks for participating ! 83 | 84 | ## Contact 85 | 86 | Enzo Sagnelonge - e.sagnelonge@axone-io.com 87 | 88 | AXONE-IO - contact@axone-io.com - https://www.axone-io.com/ 89 | 90 | ## License 91 | 92 | This project is licensed by Beerware. Please see the LICENSE.md file for more information. 93 | --------------------------------------------------------------------------------