#!/bin/bash VERSION="1.1.0" GITHUB="https://github.com/jreinke/modgit" AUTHOR="Johann Reinke" SCRIPT=${0##*/} IFS=$'\n' USAGE="\ $(tput bold)modgit v$VERSION by $AUTHOR$(tput sgr0) Fork me at $GITHUB $(tput bold)Global Commands:$(tput sgr0) $SCRIPT [options] ----------------------------------------------------------------------------------- init Initialize .modgit/ folder ls List installed modules up-all Update all installed modules rm-all Remove all installed modules Available options: -n Dry run mode (show what would be done) -v Show modgit version -h Show this help $(tput bold)Module Commands:$(tput sgr0) $SCRIPT [options] [] ----------------------------------------------------------------------------------- add Install a module by cloning specified git repository up Update specified module rm Remove specified module info Show information about a specific module files List deployed files of specified module proxy Run git command into specified module Available options: -n Dry run mode (show what would be done) -f Force all files and folders to be deployed (ignore modman mapping file) -i Include filters (only for add command) Example: $SCRIPT add $(tput bold)-i lib/ -i foo/:bar/$(tput sgr0) my_module https://github.com/account/repository.git => will deploy only lib/ (to lib/) and foo/ (to bar/) -e Exclude filters (only for add command) Example: $SCRIPT add $(tput bold)-e lib/tests/ -e lib/README.txt$(tput sgr0) my_module https://github.com/account/repository.git => will exclude both directory lib/tests/ and file lib/README.txt -b Specify a repository branch (only for add command) Example: $SCRIPT add $(tput bold)-b 1.0-stable$(tput sgr0) my_module https://github.com/account/repository.git => will checkout 1.0-stable branch of specified repository -t Specify a repository tag (only for add and update command) Example: $SCRIPT add $(tput bold)-t 1.2.0$(tput sgr0) my_module https://github.com/account/repository.git => will checkout 1.2.0 tag of specified repository " # Case-insensitive for regex matching shopt -s nocasematch DRY_RUN=0 IGNORE_MODMAN=0 ACTION="" INCLUDES="" EXCLUDES="\ readme.* about.* license.* copyright.* changelog.* credit.* faq.* \.travis.* \.git.* modman composer\.json" BRANCH="master" TAG="" # Echo in bold echo_b() { if [ "$1" = "-e" ]; then echo -e "$(tput bold)$2$(tput sgr0)" else echo "$(tput bold)$1$(tput sgr0)" fi } show_help() { echo -e "$USAGE" } # Fatal error fault() { echo_b "-e" "ERROR: $1" exit 1 } add_include() { if [ -z "$INCLUDES" ]; then INCLUDES="$1" else INCLUDES="$INCLUDES\n$1" fi } add_exclude() { if [ -z "$EXCLUDES" ]; then EXCLUDES="$1" else EXCLUDES="$EXCLUDES\n$1" fi } # Show help if asked or if no argument specified if [ "$1" = "help" ] || [ -z "$1" ]; then show_help exit 0 fi REGEX_ACTION="(list|info|files|ls|clone|add|update|up|update-all|up-all|remove|rm|remove-all|rm-all|proxy)" # Accept action as first argument if [[ "$1" =~ $REGEX_ACTION ]]; then ACTION="$1"; shift fi # Handle options while getopts ":nvhfi:e:b:t:" opt; do case $opt in h) show_help exit 0 ;; v) echo_b "$SCRIPT v$VERSION by $AUTHOR" echo "Fork me at $GITHUB" exit 0 ;; n) DRY_RUN=1 shift $((OPTIND-1)); OPTIND=1 ;; f) IGNORE_MODMAN=1 shift $((OPTIND-1)); OPTIND=1 ;; i) add_include "$OPTARG" shift $((OPTIND-1)); OPTIND=1 ;; e) add_exclude "$OPTARG" shift $((OPTIND-1)); OPTIND=1 ;; b) BRANCH="$OPTARG" shift $((OPTIND-1)); OPTIND=1 ;; t) TAG="$OPTARG" shift $((OPTIND-1)); OPTIND=1 ;; \?) echo "Invalid option: -$OPTARG" >&2 exit 1 ;; :) echo "Option -$OPTARG requires an argument" >&2 exit 1 ;; esac done # Initializes .modgit folder in root dir then quit if [ "$1" = "init" ]; then [ -d ".modgit" ] && fault "$SCRIPT is already initialized" mkdir .modgit || fault "Could not create .modgit directory" echo_b "Initialized $SCRIPT at $(pwd)/.modgit/" exit 0 fi if [ $DRY_RUN -eq 1 ]; then echo_b "DRY RUN MODE ON" sleep 1 fi # Checks if module dir exists or quit require_module_dir() { [ -d "$MODGIT_DIR/$1" ] || fault "Module '$1' does not exist" return 0 } # Safe module removal remove_module_dir() { local module="$1" local module_dir="$MODGIT_DIR/$module" if [ -n "$module" ] && [ -d "$module_dir" ]; then rm -rf "$module_dir" 2>/dev/null fi return 0 } # Deletes deployed files of specified module delete_files() { local module="$1" local module_dir="$MODGIT_DIR/$module" local deployed_file="$module_dir/deployed.modgit" [ $DRY_RUN -eq 1 ] && echo_b "Would remove files:" || echo_b "Removing files:" for line in $(cat "$deployed_file" 2>/dev/null); do if ! [ -z "$line" ] && [ -f "$ROOT/$line" ]; then echo $line # Remove deployed file and empty dirs if [ $DRY_RUN -eq 0 ]; then rm "$ROOT/$line" 2>/dev/null prev_dir="" dir=$(dirname $line) while [ "$dir" != "$prev_dir" ]; do prev_dir=$dir test_dir="$ROOT/$dir" if ! [ "$(ls -A $test_dir)" ]; then rmdir $test_dir fi dir=${dir%/*} done fi fi done return 0 } # Removes specified module delete_module() { local module="$1" local module_dir="$MODGIT_DIR/$module" require_module_dir "$module" if [ $DRY_RUN -eq 0 ]; then read -p "Are you sure you want to remove '$module' module? (y/n): " confirm [ "$confirm" != "y" ] && [ "$confirm" != "" ] && echo "Aborting..." && return 0 fi echo_b "Removing '$module' module..." sleep 1 delete_files "$module" || return 1 [ $DRY_RUN -eq 1 ] && return 0 remove_module_dir "$module" return $? } # Copies specified file to root directory deploy_file() { local root_tmp="${ROOT//\//\\/}\\/" echo $(echo "$2" | sed "s/^$root_tmp//g") if [ $DRY_RUN -eq 0 ]; then mkdir -p $(dirname $2) && cp "$1" "$2" fi } # Synchronizes files from module dir to root dir, then store deployed files for easy remove move_files() { local module="$1" local module_dir="$MODGIT_DIR/$module" local source_dir="$module_dir/source" local includes_file="$module_dir/includes.modgit" local excludes_file="$module_dir/excludes.modgit" local modman_file="$source_dir/modman" local deployed_file="$module_dir/deployed.modgit" > "$deployed_file" # empty file cd "$source_dir" || return 1 [ $DRY_RUN -eq 1 ] && echo_b "Would deploy:" || echo_b "Deploying:" for file in $(find . -type f -not -iwholename '*.git*' | sed 's/^\.\///'); do # Copy file by default copy=1 target="$file" # Include filters if [ -s "$includes_file" ]; then copy=0 for filter in $(cat "$includes_file"); do src=$(echo $filter | cut -d: -f1) if [ -z "$src" ]; then continue fi if [[ "$file" =~ ^$src ]]; then copy=1 # Handle optional different target real=$(echo $filter | cut -d: -f2) if [ "$src" != "$real" ]; then tmp_src=$(echo $src | sed 's/^[\/]*//;s/[\/]*$//') tmp_src="$tmp_src/" tmp_src="${tmp_src//\//\\/}" tmp_real=$(echo $real | sed 's/^[\/]*//;s/[\/]*$//') tmp_real="$tmp_real/" tmp_real="${tmp_real//\//\\/}" target=$(echo $file | sed "s/$tmp_src/$tmp_real/g") fi break fi done fi # Exclude filters if [ -s "$excludes_file" ] && [ $copy -eq 1 ]; then for filter in $(cat "$excludes_file"); do if [[ "$file" =~ ^$filter ]]; then copy=0 break fi done fi if [ $copy -eq 1 ]; then # Handle modman file if [ $IGNORE_MODMAN -eq 0 ] && [ -s "$modman_file" ]; then IFS=$'\n' for line in $(cat "$modman_file"); do if [ -z "$line" ] || [[ $line =~ ^# ]] || [[ $line =~ ^@ ]]; then continue fi IFS=$' \t' line="${line/\*/}" # remove * char set -- $line # set $1 and $2 if [[ "$file" =~ ^$1 ]]; then # Remove trailing slashes and escape paths for sed src=$(echo $1 | sed 's/^[\/]*//;s/[\/]*$//') dest=$(echo $2 | sed 's/^[\/]*//;s/[\/]*$//') src="${src//\//\\/}" dest="${dest//\//\\/}" target=$(echo $file | sed "s/$src/$dest/g") deploy_file "$file" "$ROOT/$target" echo "$target" >> "$deployed_file" fi done else deploy_file "$file" "$ROOT/$target" echo "$target" >> "$deployed_file" fi fi done return 0 } # Creates module dir, clones git repo and optionally stores include/exclude filters create_module() { local repo="$1" local module="$2" local module_dir="$MODGIT_DIR/$module"; shift 2 local source_dir="$module_dir/source" cd "$MODGIT_DIR" echo_b "Cloning $repo..." if ! git clone --quiet -- $repo "$source_dir"; then remove_module_dir "$module" fault "An error occurred while cloning repository" fi echo "$repo" > "$module_dir/repository.modgit" cd "$source_dir" # Ignore chmod changes in future git config core.filemode false if [ -n "$TAG" ]; then if [ -z $(git tag -l "$TAG") ]; then remove_module_dir "$module" fault "Tag '$TAG' does not exist" fi if ! git checkout --quiet "$TAG"; then remove_module_dir "$module" fault "An error occurred while fetching tag $TAG" fi echo "$TAG" > "$module_dir/tag.modgit" elif ! git checkout --quiet "$BRANCH"; then remove_module_dir "$module" fault "An error occurred while fetching branch $BRANCH" fi echo "$BRANCH" > "$module_dir/branch.modgit" echo "Fetching submodules..." if ! git submodule --quiet update --init --recursive; then remove_module_dir "$module" fault "An error occurred while cloning submodules of $repo" fi # Save includes filter if not empty [ -z "$INCLUDES" ] || echo -e "$INCLUDES" > "$module_dir/includes.modgit" # Save excludes filter if not empty [ -z "$EXCLUDES" ] || echo -e "$EXCLUDES" > "$module_dir/excludes.modgit" return 0 } # Updates a module update_module() { local module="$1" local module_dir="$MODGIT_DIR/$module" local source_dir="$module_dir/source" require_module_dir "$module" echo_b "Updating '$module' module..." sleep 1 cd "$source_dir" local old_commit=$(git rev-parse HEAD) # If module was fetched with a specific tag if [ -s "$module_dir/tag.modgit" ]; then if [ -z "$TAG" ]; then local repo_tag=$(cat "$module_dir/tag.modgit") echo "'$module' module was fetched from tag '$repo_tag' and thus cannot be updated without -t option" return 1 fi if [ -z $(git tag -l "$TAG") ]; then echo "Tag '$TAG' does not exist" return 1 fi if ! git checkout --quiet "$TAG"; then echo "An error occurred while fetching tag $TAG" return 1 fi local old_tag=$(cat "$module_dir/tag.modgit") echo "Switching from tag $old_tag to $TAG" [ $DRY_RUN -eq 0 ] && echo "$TAG" > "$module_dir/tag.modgit" else # Ignoring -b option (switching branch) because not yet supported [ -s "$module_dir/branch.modgit" ] && BRANCH=$(cat "$module_dir/branch.modgit") git pull --quiet origin "$BRANCH" && git submodule --quiet update --init --recursive || return 1 fi local new_commit=$(git rev-parse HEAD) local count=$(git diff --shortstat "$old_commit" "$new_commit" | cut -d" " -f2) [ $DRY_RUN -eq 1 ] && git checkout --quiet "$old_commit" [ "$count" = "" ] && echo "No changes found" && return 0 if [ $DRY_RUN -eq 1 ]; then echo_b "Would modify:" git --no-pager diff --name-only "$old_commit" "$new_commit" echo "$count file(s) changed" return 0 fi if delete_files "$module" && move_files "$module"; then return 0 else return 1 fi } module_info() { local module="$1" local module_dir="$MODGIT_DIR/$module" require_module_dir "$module" echo_b "Information about '$module' module:" if [ -s "$module_dir/repository.modgit" ]; then local repo=$(cat "$module_dir/repository.modgit") echo "Repository: $repo" fi if [ -s "$module_dir/tag.modgit" ]; then local current_tag=$(cat "$module_dir/tag.modgit") echo "Tag: $current_tag" else local current_branch="master" [ -s "$module_dir/branch.modgit" ] && current_branch=$(cat "$module_dir/branch.modgit") echo "Branch: $current_branch" fi return 0 } module_info_inline() { local module="$1" local module_dir="$MODGIT_DIR/$module" local info="" require_module_dir "$module" if [ -s "$module_dir/repository.modgit" ]; then local repo=$(cat "$module_dir/repository.modgit") info="$info $repo" fi if [ -s "$module_dir/tag.modgit" ]; then local current_tag=$(cat "$module_dir/tag.modgit") info="$info (tag: $current_tag)" else local current_branch="master" [ -s "$module_dir/branch.modgit" ] && current_branch=$(cat "$module_dir/branch.modgit") info="$info (branch: $current_branch)" fi printf "$(tput bold)%-20s$(tput sgr0) $info\n" "$module" return 0 } require_modules() { count=$(ls -A "$MODGIT_DIR" | wc -l | sed 's/ //g') [ $count -eq 0 ] && fault "There is no module installed" } # Try to find .modgit dir ROOT=$(pwd -P) [ -d "$ROOT/.modgit" ] || fault "modgit directory not found\nRun '$SCRIPT init' in root folder of your project" # Path to .modgit MODGIT_DIR="$ROOT/.modgit" # Action is first argument if [ -z "$ACTION" ] && [[ "$1" =~ $REGEX_ACTION ]]; then ACTION="$1"; shift fi [ -z "$ACTION" ] && show_help && fault "No action specified" # Handle action case "$ACTION" in list|ls) # List all installed modules count=$(ls -A "$MODGIT_DIR" | wc -l | sed 's/ //g') for module in $(ls -1 "$MODGIT_DIR"); do [ -d "$MODGIT_DIR/$module" ] || continue; module_info_inline "$module" done echo "$count module(s) found" ;; clone|add) # Install new module module="$1"; shift [ -z "$module" ] && fault "No module specified" module_dir="$MODGIT_DIR/$module" [[ "$module" =~ [^a-z0-9_-]+ ]] && fault "You cannot add a module with a name not matching [^a-zA-Z0-9_-]+ pattern\nModule specified: $module" [ -d "$module_dir" ] && fault "A module with this name already exists" success=1 repo="$1"; shift create_module "$repo" "$module" || success=0 cd "$MODGIT_DIR" if [ $success -eq 1 ]; then if require_module_dir "$module" && move_files "$module"; then [ $DRY_RUN -eq 0 ] && echo_b "Installation complete" || remove_module_dir "$module" fi else remove_module_dir "$module" fault "Error cloning '$module', operation cancelled" fi ;; update|up) # Update specified module module="$1"; shift [ -z "$module" ] && fault "No module specified" if ! update_module "$module"; then fault "Error updating '$module', operation cancelled" fi ;; update-all|up-all) # Update all installed modules [ -n "$1" ] && fault "Too many arguments for '$ACTION' command" require_modules errors=0 for module in $(ls -1 "$MODGIT_DIR"); do [ -d "$MODGIT_DIR/$module" ] || continue; if ! update_module "$module"; then echo_b "-e" "Error occurred while updating '$module'" errors=$((errors+1)) fi done [ $DRY_RUN -eq 0 ] && echo_b "Updated all modules with $errors error(s)" ;; remove|rm) # Remove specified module module="$1"; shift [ -z "$module" ] && fault "No module specified" if ! require_module_dir "$module" || ! delete_module "$module"; then echo_b "Error removing '$module', operation cancelled" fi ;; remove-all|rm-all) # Remove all installed modules [ -n "$1" ] && fault "Too many arguments for '$ACTION' command" require_modules errors=0 for module in $(ls -1 "$MODGIT_DIR"); do [ -d "$MODGIT_DIR/$module" ] || continue if ! delete_module "$module"; then echo_b "-e" "Error occurred while removing '$module' module" errors=$((errors+1)) fi done [ $DRY_RUN -eq 0 ] && echo_b "Removed desired modules with $errors error(s)" ;; info) # Show information about specified module module="$1"; shift [ -z "$module" ] && fault "No module specified" module_info "$module" ;; files) # List deployed files of specified module module="$1"; shift [ -z "$module" ] && fault "No module specified" require_module_dir "$module" echo_b "Deployed files of '$module' module:" sleep 1 module_dir="$MODGIT_DIR/$module" cat "$module_dir/deployed.modgit" ;; proxy) # Run git command into specified module module="$1"; shift [ -z "$module" ] && fault "No module specified" require_module_dir "$module" cd "$MODGIT_DIR/$module/source" git "$@" ;; *) show_help echo_b "Invalid action: $ACTION" exit 1 esac