├── doc ├── logo-med.png ├── travis-favicon.png ├── getting-started.png ├── screenshot-twgit.png ├── screenshot-twgit-demo.png ├── screenshot-twgit-tag.png ├── screenshot-twgit-feature.png ├── screenshot-twgit-hotfix.png └── screenshot-twgit-release.png ├── tests ├── resources │ └── csv_tags ├── inc │ ├── testFunction.sh │ ├── testShellCode.sh │ ├── bootstrap.inc.php │ ├── codeCoverage.sh │ └── TwgitTestCase.php ├── lib │ ├── Shell │ │ ├── PathStatus.php │ │ └── Interface.php │ ├── ClassLoader.php │ └── ErrorHandler.php ├── TwgitDyslexiaTest.php ├── TwgitOptionsHandlerTest.php ├── TwgitCommonToolsTest.php ├── TwgitDemoTest.php ├── TwgitCommonGettersTest.php ├── TwgitTagTest.php ├── TwgitCommonProcessingTest.php ├── TwgitHelpTest.php ├── TwgitCUITest.php ├── TwgitCommonAssertsTest.php └── TwgitHotfixTest.php ├── .gitignore ├── install ├── zsh_completion.sh ├── bash_git.sh ├── completion │ └── zsh │ │ └── _twgit ├── command_prompt_screenshots.sh └── bash_completion.sh ├── .travis.yml ├── conf ├── phpunit-dist.xml ├── phpunit-dist.php └── twgit-dist.sh ├── LICENSE ├── Makefile ├── inc ├── connectors │ ├── feature_subject_pivotal.sh │ ├── feature_subject_github.sh │ ├── feature_subject_trello.sh │ ├── feature_subject_jira.sh │ ├── feature_subject_redmine.sh │ └── feature_subject_gitlab.sh ├── dyslexia.inc.sh ├── compatibility.inc.sh ├── twgit_tag.inc.sh ├── options_handler.inc.sh ├── coloredUI.inc.sh ├── twgit_hotfix.inc.sh └── twgit_demo.inc.sh ├── README.md ├── makefile.sh └── twgit /doc/logo-med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/logo-med.png -------------------------------------------------------------------------------- /doc/travis-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/travis-favicon.png -------------------------------------------------------------------------------- /doc/getting-started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/getting-started.png -------------------------------------------------------------------------------- /doc/screenshot-twgit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit.png -------------------------------------------------------------------------------- /doc/screenshot-twgit-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit-demo.png -------------------------------------------------------------------------------- /doc/screenshot-twgit-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit-tag.png -------------------------------------------------------------------------------- /doc/screenshot-twgit-feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit-feature.png -------------------------------------------------------------------------------- /doc/screenshot-twgit-hotfix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit-hotfix.png -------------------------------------------------------------------------------- /doc/screenshot-twgit-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twenga/twgit/HEAD/doc/screenshot-twgit-release.png -------------------------------------------------------------------------------- /tests/resources/csv_tags: -------------------------------------------------------------------------------- 1 | $Id$ 2 | ------- 3 | $Id:1.0.2$ 4 | ------- 5 | $id$ 6 | ------- 7 | $Id:1.0.2$ $Id$ 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.settings 3 | /.git 4 | /.lastupdate 5 | /.buildpath 6 | /.history.error 7 | /.history.log 8 | /.features_subject 9 | /.twgit_features_subject 10 | /conf/phpunit.xml 11 | /conf/phpunit.php 12 | /conf/twgit.sh 13 | /.twgit 14 | /.idea 15 | -------------------------------------------------------------------------------- /install/zsh_completion.sh: -------------------------------------------------------------------------------- 1 | # COMPLETION SETTINGS 2 | # add custom completion scripts 3 | current_path=$PWD 4 | mypath=$(readlink -f $0) 5 | current_path=$(dirname "$mypath") 6 | fpath=("${current_path}/completion/zsh" $fpath) 7 | export fpath 8 | 9 | # compsys initialization 10 | autoload -U compinit 11 | compinit 12 | 13 | # show completion menu when number of options is at least 2 14 | zstyle ':completion:*' menu select=2 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # See: http://about.travis-ci.org/docs/user/build-configuration/ 2 | 3 | language: php 4 | php: 5 | - 5.6 6 | 7 | before_script: 8 | - cp conf/twgit-dist.sh conf/twgit.sh 9 | - cp conf/phpunit-dist.php conf/phpunit.php 10 | - git config --global user.name 'Firstname Lastname' 11 | - git config --global user.email 'firstname.lastname@xyz.com' 12 | script: phpunit --configuration conf/phpunit-dist.xml 13 | after_script: 14 | - tests/inc/codeCoverage.sh <(find $(pwd)/inc -type f -name '*.sh' && echo $(pwd)/twgit) <(find $(pwd) -type f -name '*Test.php') 15 | 16 | notifications: 17 | on_success: always 18 | on_failure: always 19 | -------------------------------------------------------------------------------- /conf/phpunit-dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | ../tests 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /install/bash_git.sh: -------------------------------------------------------------------------------- 1 | function parse_git_branch { 2 | git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/' 3 | } 4 | 5 | function parse_git_status { 6 | noupdated=`git status --porcelain 2> /dev/null | grep -E "^ (M|D)" | wc -l` 7 | nocommitted=`git status --porcelain 2> /dev/null | grep -E "^(M|A|D|R|C)" | wc -l` 8 | 9 | if [[ $noupdated -gt 0 ]]; then echo -n "*"; fi 10 | if [[ $nocommitted -gt 0 ]]; then echo -n "+"; fi 11 | } 12 | 13 | RED="\[\033[01;31m\]" 14 | YELLOW="\[\033[01;33m\]" 15 | GREEN="\[\033[01;32m\]" 16 | BLUE="\[\033[01;34m\]" 17 | NC="\[\033[0m\]" 18 | 19 | case $TERM in 20 | xterm*) 21 | TITLEBAR='\[\e]0;\u@\h: \w\a\]'; 22 | ;; 23 | *) 24 | TITLEBAR=""; 25 | ;; 26 | esac 27 | 28 | PS1="${TITLEBAR}$RED\$(date +%H:%M) $GREEN\u@\h $BLUE\w$YELLOW\$(parse_git_branch)\$(parse_git_status) $BLUE\$ $NC" 29 | -------------------------------------------------------------------------------- /tests/inc/testFunction.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Call a function of common.inc.sh after loading Shell config files. 5 | # e.g.: /bin/bash testFunction.sh process_fetch x 6 | # 7 | # @author Geoffroy Aubry 8 | # 9 | 10 | 11 | 12 | # Parameters: 13 | sCommonFunction="$1"; shift 14 | 15 | # Pre config: 16 | # Absolute path of the top-level directory of the current user repository: 17 | TWGIT_USER_REPOSITORY_ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" 18 | 19 | # Includes: 20 | . /tmp/conf-twgit.sh 21 | . $TWGIT_INC_DIR/common.inc.sh 22 | 23 | # Post config: 24 | # TWGIT_USER_REPOSITORY_ROOT_DIR is absolute path of the top-level directory of the current user repository 25 | TWGIT_FEATURES_SUBJECT_PATH="$TWGIT_USER_REPOSITORY_ROOT_DIR/$TWGIT_FEATURES_SUBJECT_FILENAME" 26 | 27 | # Execution: 28 | if [ ! -z "$sCommonFunction" ]; then 29 | $sCommonFunction "$@" 30 | fi 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | 3 | Copyright (c) 2011 Twenga SA 4 | Copyright (c) 2012-2013 Geoffroy Aubry 5 | Copyright (c) 2012 Laurent Toussaint 6 | Copyright (c) 2013 Cyrille Hemidy 7 | Copyright (c) 2013 Geoffroy Letournel 8 | Copyright (c) 2013 Sebastien Hanicotte 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | -------------------------------------------------------------------------------- /tests/inc/testShellCode.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Execute code calling functions of common.inc.sh after loading Shell config files. 5 | # e.g.: /bin/bash testShellCode.sh 'process_options x -aV; isset_option a; echo $?' 6 | # 7 | # @author Geoffroy Aubry 8 | # @author Laurent Toussaint 9 | # 10 | 11 | 12 | 13 | # Parameters: 14 | sCmds="$1"; shift 15 | 16 | # Pre config: 17 | # Absolute path of the top-level directory of the current user repository: 18 | TWGIT_USER_REPOSITORY_ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" 19 | 20 | # Includes: 21 | . /tmp/conf-twgit.sh 22 | . $TWGIT_INC_DIR/common.inc.sh 23 | 24 | # Post config: 25 | # TWGIT_USER_REPOSITORY_ROOT_DIR is absolute path of the top-level directory of the current user repository 26 | TWGIT_FEATURES_SUBJECT_PATH="$TWGIT_USER_REPOSITORY_ROOT_DIR/$TWGIT_FEATURES_SUBJECT_FILENAME" 27 | 28 | # Execution: 29 | rFile="${TWGIT_TMP_DIR}/file.$$.$RANDOM" 30 | echo "$sCmds" > $rFile 31 | . $rFile 32 | rm -f $rFile 33 | -------------------------------------------------------------------------------- /tests/inc/bootstrap.inc.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | include_once(__DIR__ . '/../../conf/phpunit.php'); 11 | include_once(TWGIT_TESTS_LIB_DIR . '/ClassLoader.php'); 12 | 13 | set_include_path( 14 | TWGIT_ROOT_DIR . PATH_SEPARATOR . 15 | get_include_path() 16 | ); 17 | 18 | ClassLoader::register('', TWGIT_TESTS_LIB_DIR); 19 | ClassLoader::register('', TWGIT_TESTS_INC_DIR); 20 | 21 | $GLOBALS['oErrorHandler'] = new ErrorHandler( 22 | TWGIT_DISPLAY_ERRORS, 23 | TWGIT_ERROR_LOG_PATH, 24 | TWGIT_ERROR_LEVEL, 25 | TWGIT_AUTH_ERROR_SUPPR_OP 26 | ); 27 | 28 | // Avoid update process of Twgit: 29 | touch(TWGIT_ROOT_DIR . '/.lastupdate'); 30 | 31 | // Overload conf/twgit.sh with PHP defines in conf/phpunit.php. 32 | // Result into TWGIT_TMP_DIR . '/conf-twgit.sh' 33 | $sConf = file_get_contents(TWGIT_ROOT_DIR . '/conf/twgit.sh'); 34 | $aAllDefines = get_defined_constants(true); 35 | $aUserDefines = $aAllDefines['user']; 36 | foreach ($aUserDefines as $sKey => $sValue) { 37 | if (strpos($sKey, 'TWGIT_') === 0) { 38 | $sConf = preg_replace("/^$sKey=.*$/m", "$sKey='$sValue'", $sConf); 39 | } 40 | } 41 | file_put_contents(TWGIT_TMP_DIR . '/conf-twgit.sh', $sConf); 42 | -------------------------------------------------------------------------------- /conf/phpunit-dist.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | // Paths 9 | define('TWGIT_ROOT_DIR', realpath(__DIR__ . '/..')); 10 | define('TWGIT_TMP_DIR', '/tmp'); 11 | define('TWGIT_TESTS_DIR', TWGIT_ROOT_DIR . '/tests'); 12 | define('TWGIT_TESTS_LIB_DIR', TWGIT_TESTS_DIR . '/lib'); 13 | define('TWGIT_TESTS_INC_DIR', TWGIT_TESTS_DIR . '/inc'); 14 | 15 | // Error handler 16 | define('TWGIT_DISPLAY_ERRORS', true); 17 | define('TWGIT_ERROR_LOG_PATH', ''); 18 | define('TWGIT_ERROR_LEVEL', -1); 19 | define('TWGIT_AUTH_ERROR_SUPPR_OP', true); 20 | 21 | // Exec 22 | define('TWGIT_BASH_EXEC', '/bin/bash'); 23 | define('TWGIT_EXEC', TWGIT_BASH_EXEC . ' ' . TWGIT_ROOT_DIR . '/twgit'); 24 | 25 | // Branches 26 | define('TWGIT_ORIGIN', 'testorigin'); 27 | define('TWGIT_STABLE', 'teststable'); 28 | 29 | // Connector 30 | define('TWGIT_FEATURE_SUBJECT_CONNECTOR', ''); // deactivated! 31 | 32 | // Other 33 | define('TWGIT_REPOSITORY_ORIGIN_DIR', TWGIT_TMP_DIR . '/origin'); 34 | define('TWGIT_REPOSITORY_LOCAL_DIR', TWGIT_TMP_DIR . '/local'); 35 | define('TWGIT_REPOSITORY_SECOND_LOCAL_DIR', TWGIT_TMP_DIR . '/local2'); 36 | define('TWGIT_REPOSITORY_SECOND_REMOTE_DIR', TWGIT_TMP_DIR . '/second'); 37 | define('TWGIT_REPOSITORY_THIRD_REMOTE_DIR', TWGIT_TMP_DIR . '/third'); 38 | -------------------------------------------------------------------------------- /tests/lib/Shell/PathStatus.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://www.apache.org/licenses/LICENSE-2.0 9 | * @see Shell_Interface::getPathStatus() 10 | */ 11 | final class Shell_PathStatus 12 | { 13 | /** 14 | * Le chemin n'existe pas. 15 | * @var int 16 | */ 17 | const STATUS_NOT_EXISTS = 0; 18 | 19 | /** 20 | * Le chemin est un fichier. 21 | * @var int 22 | */ 23 | const STATUS_FILE = 1; 24 | 25 | /** 26 | * Le chemin est un répertoire. 27 | * @var int 28 | */ 29 | const STATUS_DIR = 2; 30 | 31 | /** 32 | * Le chemin est un lien symbolique cassé. 33 | * @var int 34 | */ 35 | const STATUS_BROKEN_SYMLINK = 10; 36 | 37 | /** 38 | * Le chemin est un lien symbolique pointant sur un fichier. 39 | * @var int 40 | */ 41 | const STATUS_SYMLINKED_FILE = 11; 42 | 43 | /** 44 | * Le chemin est un lien symbolique pointant sur un répertoire. 45 | * @var int 46 | */ 47 | const STATUS_SYMLINKED_DIR = 12; 48 | 49 | /** 50 | * Classe de constantes, non instanciable. 51 | */ 52 | private function __construct() 53 | { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## 4 | # Installation of Twgit. 5 | # 6 | # In the directory of your choice, e.g. ~/twgit: 7 | # git clone git@github.com:Twenga/twgit.git . 8 | # sudo make install 9 | # 10 | # 11 | # 12 | # Copyright (c) 2011 Twenga SA 13 | # Copyright (c) 2012-2013 Geoffroy Aubry 14 | # Copyright (c) 2014 Romain Derocle 15 | # 16 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 17 | # with the License. You may obtain a copy of the License at 18 | # 19 | # http://www.apache.org/licenses/LICENSE-2.0 20 | # 21 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 22 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 23 | # for the specific language governing permissions and limitations under the License. 24 | # 25 | # @copyright 2011 Twenga SA 26 | # @copyright 2012-2013 Geoffroy Aubry 27 | # @copyright 2014 Romain Derocle 28 | # @license http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | 31 | ROOT_DIR:=$(shell pwd) 32 | BIN_DIR:="/usr/local/bin" 33 | CURRENT_SHELL=$(shell basename "$$SHELL") 34 | CURRENT_SHELL_CMD=$(shell which $(CURRENT_SHELL)) 35 | 36 | .PHONY: all doc help install uninstall 37 | 38 | default: help 39 | 40 | doc: 41 | @$(CURRENT_SHELL) $(ROOT_DIR)/makefile.sh doc 42 | 43 | help: 44 | @$(CURRENT_SHELL) $(ROOT_DIR)/makefile.sh help 45 | 46 | install: 47 | @$(CURRENT_SHELL_CMD) $(ROOT_DIR)/makefile.sh install 48 | 49 | uninstall: 50 | @$(CURRENT_SHELL) $(ROOT_DIR)/makefile.sh uninstall 51 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_pivotal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2017 Alexandre Guidet 9 | # 10 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 11 | # with the License. You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | 16 | ## 17 | # Retrieve and display subject of a Pivotal's story. 18 | # 19 | # @param string $1 story number 20 | # 21 | issue="$1" 22 | url="$(printf "%s/projects/%s/stories/%s" \ 23 | "https://www.pivotaltracker.com/services/v5" \ 24 | "$TWGIT_FEATURE_SUBJECT_PIVOTAL_PROJECT_ID" \ 25 | "$issue")" 26 | 27 | cmd="curl -X GET -H \"X-TrackerToken: ${TWGIT_FEATURE_SUBJECT_PIVOTAL_API_TOKEN}\" ${url}" 28 | 29 | data=$(eval $cmd) 30 | 31 | # Python or PHP ? 32 | language='?' 33 | which python 1>/dev/null 2>&1 34 | if [ $? -eq 0 ]; then 35 | language='python' 36 | else 37 | which php 1>/dev/null 2>&1 38 | if [ $? -eq 0 ]; then 39 | language='php' 40 | fi 41 | fi 42 | 43 | # Convert JSON with Python or PHP: 44 | if [ "$language" = 'python' ]; then 45 | if [ ! -z "$data" ]; then 46 | echo $data | python -c "import json,sys;s=sys.stdin.read();s=s.replace('\r\n', '');s=json.loads(s);print s['name'].encode('utf8');" 2>/dev/null 47 | fi 48 | elif [ "$language" = 'php' ]; then 49 | if [ ! -z "$data" ]; then 50 | echo $data | php -r '$o = json_decode(file_get_contents("php://stdin")); echo $o->name;' 2>/dev/null 51 | fi 52 | else 53 | echo "Language '$language' not handled!" >&2 54 | exit 1 55 | fi 56 | 57 | exit 0 58 | -------------------------------------------------------------------------------- /install/completion/zsh/_twgit: -------------------------------------------------------------------------------- 1 | #compdef twgit 2 | 3 | _twgit() { 4 | 5 | local curcontext="$curcontext" state line 6 | typeset -A opt_args 7 | 8 | _arguments \ 9 | '1: :->command' \ 10 | '2: :->action' \ 11 | '3: :->option' 12 | 13 | case $state in 14 | (command) 15 | compadd "$@" clean feature demo help init hotfix release tag update 16 | ;; 17 | (action) 18 | case $words[2] in 19 | (clean) ret=0 20 | ;; 21 | (feature) 22 | compadd "$@" committers help list merge-into-release merge-into-hotfix migrate push remove start status what-changed 23 | ;; 24 | (demo) compadd "$@" help list merge-feature update-features merge-demo push remove start status 25 | ;; 26 | (hotfix) compadd "$@" finish help list push remove start 27 | ;; 28 | (release) compadd "$@" committers finish merge-demo help list push remove reset start 29 | ;; 30 | (tag) compadd "$@" help list 31 | ;; 32 | esac 33 | ;; 34 | (option) 35 | case $words[2] in 36 | (feature) 37 | case $words[3] in 38 | (committers) 39 | compadd $(git branch --no-color -r | grep "feature-" | sed 's/^[* ]*//' | sed 's#^origin/feature-##' | tr '\n' ' ') 40 | ;; 41 | esac 42 | ;; 43 | (release) 44 | case $words[3] in 45 | (finish) 46 | compadd $(git branch --no-color -r | grep "release-" | sed 's/^[* ]*//' | sed 's#^origin/release-##' | tr '\n' ' ') 47 | ;; 48 | esac 49 | ;; 50 | esac 51 | ;; 52 | esac 53 | } 54 | 55 | _twgit "$@" 56 | -------------------------------------------------------------------------------- /install/command_prompt_screenshots.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Generate screenshots of help on command prompt. 5 | # 6 | # Usage: bash command_prompt_screenshots.sh 7 | # 8 | # Need: 9 | # sudo apt-get install imagemagick 10 | # 11 | # 12 | # 13 | # Copyright (c) 2012-2013 Geoffroy Aubry 14 | # 15 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 16 | # with the License. You may obtain a copy of the License at 17 | # 18 | # http://www.apache.org/licenses/LICENSE-2.0 19 | # 20 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 21 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 22 | # for the specific language governing permissions and limitations under the License. 23 | # 24 | # @copyright 2012-2013 Geoffroy Aubry 25 | # @license http://www.apache.org/licenses/LICENSE-2.0 26 | # 27 | 28 | 29 | 30 | root_dir="$1" 31 | tmp_dir='/tmp' 32 | doc_dir="$root_dir/doc" 33 | sleep_time=3 34 | gnome_terminal_title="twgit" 35 | 36 | function take_snapshot () { 37 | local twgit_cmd="$@" 38 | local screenshot_filename="screenshot-${twgit_cmd// /-}" 39 | local error_code 40 | 41 | gnome-terminal --title='twgit' --disable-factory --full-screen --profile=twgit -e "bash -c \"echo -e '\\033[01;33m$ \033[0m$twgit_cmd'; bash $root_dir/$twgit_cmd; echo -en '\\033[01;33m$ '; sleep $((sleep_time + 3))\"" & PID=$! 42 | sleep $sleep_time 43 | local WID=$(xwininfo -root -tree | grep "\"$gnome_terminal_title\":" | awk '{ print $1 }') 44 | xwd -nobdrs -out $tmp_dir/$screenshot_filename.xwd -id $WID && \ 45 | convert -gravity NorthEast -chop 20x24 -trim -border 2x2 -bordercolor "#000000" "$tmp_dir/$screenshot_filename.xwd" "$doc_dir/$screenshot_filename.png" && \ 46 | rm -f "$tmp_dir/$screenshot_filename.xwd" 47 | error_code=$? 48 | kill -9 $PID 49 | return $error_code 50 | } 51 | 52 | cmds="twgit 53 | twgit feature 54 | twgit demo 55 | twgit hotfix 56 | twgit release 57 | twgit tag" 58 | IFS="$(echo -e "\n\r")" 59 | for cmd in $cmds; do 60 | take_snapshot "$cmd" || exit $? 61 | done 62 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_github.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # Copyright (c) 2014 Laurent Toussaint 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2011 Twenga SA 22 | # @copyright 2012-2013 Geoffroy Aubry 23 | # @copyright 2014 Laurent Toussaint 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | 29 | ## 30 | # Retrieve and display subject of a Github's issue. 31 | # Compatible Github API v3: http://developer.github.com/v3/. 32 | # 33 | # @param string $1 issue name 34 | # 35 | issue="$1" 36 | url="$(printf "https://api.github.com/repos/%s/%s/issues/%s" \ 37 | "$TWGIT_FEATURE_SUBJECT_GITHUB_USER" \ 38 | "$TWGIT_FEATURE_SUBJECT_GITHUB_REPOSITORY" \ 39 | "$issue")" 40 | 41 | if ${has_wget}; then 42 | cmd="wget --no-check-certificate --timeout=3 --user-agent=Twenga-twgit -q -O - --no-cache" 43 | else 44 | cmd="curl --insecure --max-time 3 --user-agent Twenga-twgit --silent -H \"Cache-control: no-cache\"" 45 | fi 46 | 47 | # Python or PHP ? 48 | language='?' 49 | which python 1>/dev/null 2>&1 50 | if [ $? -eq 0 ]; then 51 | language='python' 52 | else 53 | which php 1>/dev/null 2>&1 54 | if [ $? -eq 0 ]; then 55 | language='php' 56 | fi 57 | fi 58 | 59 | # Convert JSON with Python or PHP: 60 | if [ "$language" = 'python' ]; then 61 | ($cmd $url | python -c 'import sys,json;s=sys.stdin.read(); 62 | if s!="": data=json.loads(s); print data["title"].encode("utf8")') 63 | 2>/dev/null 64 | elif [ "$language" = 'php' ]; then 65 | ($cmd $url \ 66 | | php -r '$o = json_decode(file_get_contents("php://stdin")); if ($o !== NULL) {print_r($o->title);}') 67 | 2>/dev/null 68 | else 69 | echo "Language '$language' not handled!" >&2 70 | exit 1 71 | fi 72 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_trello.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2014 Julien Pottier 9 | # Copyright (c) 2014 Laurent Toussaint 10 | # 11 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 12 | # with the License. You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 17 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 18 | # for the specific language governing permissions and limitations under the License. 19 | # 20 | # @copyright 2014 Julien Pottier 21 | # @copyright 2014 Laurent Toussaint 22 | # @license http://www.apache.org/licenses/LICENSE-2.0 23 | # 24 | 25 | 26 | 27 | ## 28 | # Retrieve and display subject of a Trello's issue. 29 | # 30 | # @param string $1 issue number or project name 31 | # 32 | issue="$1" 33 | url="$(printf "%s/1/cards/%s?key=%s\&token=%s" \ 34 | "$TWGIT_FEATURE_SUBJECT_TRELLO_DOMAIN" \ 35 | "$issue" \ 36 | "$TWGIT_FEATURE_SUBJECT_TRELLO_APPLICATION_KEY" \ 37 | "$TWGIT_FEATURE_SUBJECT_TRELLO_TOKEN")" 38 | 39 | if ${has_wget}; then 40 | cmd="wget --no-check-certificate --timeout=3 -q -O - --no-cache" 41 | else 42 | cmd="curl --insecure --max-time 3 --silent -H \"Cache-control: no-cache\"" 43 | fi 44 | cmd="${cmd} ${url}" 45 | 46 | data=$(eval $cmd) 47 | 48 | # Python or PHP ? 49 | language='?' 50 | which python 1>/dev/null 2>&1 51 | if [ $? -eq 0 ]; then 52 | language='python' 53 | else 54 | which php 1>/dev/null 2>&1 55 | if [ $? -eq 0 ]; then 56 | language='php' 57 | fi 58 | fi 59 | 60 | # Convert JSON with Python or PHP: 61 | if [ "$language" = 'python' ]; then 62 | data=$(eval $cmd) 63 | if [ ! -z "$data" ]; then 64 | echo $data | python -c "import json,sys;s=sys.stdin.read();s=s.replace('\r\n', '');s=json.loads(s);print s['name'].encode('utf8');" 2>/dev/null 65 | fi 66 | elif [ "$language" = 'php' ]; then 67 | data=$(eval $cmd) 68 | if [ ! -z "$data" ]; then 69 | echo $data | php -r '$o = json_decode(file_get_contents("php://stdin")); echo $o->name;' 2>/dev/null 70 | fi 71 | else 72 | echo "Language '$language' not handled!" >&2 73 | exit 1 74 | fi 75 | 76 | exit 0 -------------------------------------------------------------------------------- /tests/lib/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 12 | * @license http://www.apache.org/licenses/LICENSE-2.0 13 | */ 14 | class ClassLoader 15 | { 16 | private $_sFileExtension; 17 | private $_sNamespace; 18 | private $_sIncludePath; 19 | 20 | public function __construct($sNamespace, $sIncludePath, $sFileExtension) 21 | { 22 | $this->_sNamespace = $sNamespace; 23 | $this->_sIncludePath = $sIncludePath; 24 | $this->_sFileExtension = $sFileExtension; 25 | } 26 | 27 | /** 28 | * Installs this class loader on the SPL autoload stack. 29 | */ 30 | public static function register($sNamespace='', $sIncludePath='', $sFileExtension='.php') 31 | { 32 | spl_autoload_register(array(new self($sNamespace, $sIncludePath, $sFileExtension), 'loadClass')); 33 | } 34 | 35 | /** 36 | * Loads the given class or interface. 37 | * 38 | * @param string $sClassName The name of the class to load. 39 | * @return bool 40 | */ 41 | public function loadClass($sClassName) 42 | { 43 | if ( 44 | $this->_sNamespace === '' 45 | || $this->_sNamespace.'\\' === substr($sClassName, 0, strlen($this->_sNamespace.'\\')) 46 | ) { 47 | 48 | if ($this->_sNamespace !== '' && $this->_sIncludePath !== '') { 49 | $sClassName = substr($sClassName, strlen($this->_sNamespace.'\\')); 50 | } 51 | 52 | $fileName = ''; 53 | $namespace = ''; 54 | $lastNsPos = strripos($sClassName, '\\'); 55 | if ($lastNsPos !== false) { 56 | $namespace = substr($sClassName, 0, $lastNsPos); 57 | $sClassName = substr($sClassName, $lastNsPos + 1); 58 | $fileName .= str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR; 59 | } 60 | $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $sClassName) . $this->_sFileExtension; 61 | if ($this->_sIncludePath !== '') { 62 | $fileName = $this->_sIncludePath . DIRECTORY_SEPARATOR . $fileName; 63 | } 64 | $filePath = stream_resolve_include_path($fileName); 65 | if ($filePath !== false) { 66 | require $filePath; 67 | } 68 | 69 | return ($filePath !== false); 70 | } else { 71 | return false; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /inc/dyslexia.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # dyslexia 5 | # 6 | # Provide an easy way to detect words in disorder. 7 | # Just include this script, define new words scheme, then call guess_dyslexia method. 8 | # This script is using the colored_ui to display Warning message 9 | # 10 | # 11 | # 12 | # Copyright (c) 2013 Sebastien Hanicotte 13 | # Copyright (c) 2014 Laurent Toussaint 14 | # 15 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 16 | # with the License. You may obtain a copy of the License at 17 | # 18 | # http://www.apache.org/licenses/LICENSE-2.0 19 | # 20 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 21 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 22 | # for the specific language governing permissions and limitations under the License. 23 | # 24 | # @copyright 2013 Sebastien Hanicotte 25 | # @copyright 2014 Laurent Toussaint 26 | # @license http://www.apache.org/licenses/LICENSE-2.0 27 | # 28 | 29 | 30 | 31 | declare -A dyslexia 32 | dyslexia['1a2e1f1r1t1u']='feature' 33 | dyslexia['1d1e1m1o']='demo' 34 | dyslexia['1a3e1l1r1s']='release' 35 | dyslexia['1f1h1i1o1t1x']='hotfix' 36 | dyslexia['1a1g1t']='tag' 37 | dyslexia['1a1c1e1l1n']='clean' 38 | dyslexia['2i1n1t']='init' 39 | dyslexia['1a1d1e1p1t1u']='update' 40 | dyslexia['1e1h1l1p']='help' 41 | 42 | dyslexia['1a1r1s2t']='start' 43 | dyslexia['1f1h2i1n1s']='finish' 44 | dyslexia['1a2s2t1u']='status' 45 | dyslexia['1c1e1i2m1o1r1s2t']='committers' 46 | dyslexia['2-1a5e1g1i1l1m1n1o2r1s1t']='merge-into-release' 47 | dyslexia['2-2e1f1g1h2i1m1n2o1r2t1x']='merge-into-hotfix' 48 | dyslexia['1a1e1g1i1m1r1t']='migrate' 49 | dyslexia['1h1p1s1u']='push' 50 | dyslexia['2e1m1o1r1v']='remove' 51 | dyslexia['1-2a1c1d1e1g2h1n1t1w']='what-changed' 52 | dyslexia['1i1l1s1t']='list' 53 | dyslexia['1-1a4e1f1g1m2r1t1u']='merge-feature' 54 | dyslexia['2e1r1s1t']='reset' 55 | 56 | ## 57 | # This add-on allows twgit to understand dyslexia 58 | # This can be usefull when user has some keyboard disorder. 59 | # 60 | # @param string $1 action 61 | # @testedby TwgitDyslexiaTest 62 | # 63 | function guess_dyslexia () { 64 | local word="$1" 65 | 66 | explode_word="$(echo $word | fold -w1 | sort | uniq -c | tr -d '[:space:]\n')" 67 | resolve_dyslexia="${dyslexia["$explode_word"]-}" 68 | 69 | if [[ -z $resolve_dyslexia ]]; then 70 | RETVAL="$word"; 71 | else 72 | if [[ $word != $resolve_dyslexia ]]; then 73 | CUI_displayMsg warning "Assume '$word' was '$resolve_dyslexia'…" 74 | fi 75 | RETVAL="$resolve_dyslexia" 76 | fi 77 | } 78 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_jira.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2014 Romain Derocle 9 | # Copyright (c) 2014 Geoffroy Aubry 10 | # Copyright (c) 2014 Laurent Toussaint 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2014 Romain Derocle 22 | # @copyright 2014 Geoffroy Aubry 23 | # @copyright 2014 Laurent Toussaint 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | 29 | ## 30 | # Retrieve and display subject of a Jira's issue. 31 | # 32 | # @param string $1 issue number or project name 33 | # 34 | issue="$1" 35 | if [[ "$TWGIT_FEATURE_SUBJECT_JIRA_DOMAIN" =~ ^https?:// ]]; then 36 | scheme='' 37 | else 38 | scheme='https://' 39 | fi 40 | issue_url="$scheme$TWGIT_FEATURE_SUBJECT_JIRA_DOMAIN/rest/api/latest/issue/$issue" 41 | 42 | if ${has_wget}; then 43 | cmd="wget --no-check-certificate --timeout=3 -q -O - --no-cache --header \"Authorization: Basic $TWGIT_FEATURE_SUBJECT_JIRA_CREDENTIAL_BASE64\" --header \"Content-Type: application/json\"" 44 | else 45 | cmd="curl --insecure --max-time 3 --user-agent Twenga-twgit --silent -H \"Cache-control: no-cache\" -H \"Authorization: Basic $TWGIT_FEATURE_SUBJECT_JIRA_CREDENTIAL_BASE64\" -H \"Content-Type: application/json\"" 46 | fi 47 | cmd="${cmd} ${issue_url}" 48 | 49 | # Python or PHP ? 50 | language='?' 51 | which python 1>/dev/null 2>&1 52 | if [ $? -eq 0 ]; then 53 | language='python' 54 | else 55 | which php 1>/dev/null 2>&1 56 | if [ $? -eq 0 ]; then 57 | language='php' 58 | fi 59 | fi 60 | 61 | # Convert JSON with Python or PHP: 62 | if [ "$language" = 'python' ]; then 63 | data=$(eval $cmd) 64 | if [ ! -z "$data" ]; then 65 | echo $data | python -c "import json,sys;s=sys.stdin.read();s=s.replace('\r\n', '');s=json.loads(s);print s['fields']['summary'].encode('utf8');" 2>/dev/null 66 | fi 67 | elif [ "$language" = 'php' ]; then 68 | data=$(eval $cmd) 69 | if [ ! -z "$data" ]; then 70 | echo $data | php -r '$o = json_decode(file_get_contents("php://stdin")); if (!empty($o)){print_r($o->fields->summary);}' 2>/dev/null 71 | fi 72 | else 73 | echo "Language '$language' not handled!" >&2 74 | exit 1 75 | fi 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![TwGit logo](https://github.com/Twenga/twgit/raw/stable/doc/logo-med.png)](http://twgit.twenga.com/) TwGit 2 | ========== 3 | 4 | #### [Homepage](http://twgit.twenga.com/) 5 | 6 | ## Description 7 | 8 | Twgit is a free and open source assisting tools for managing features, hotfixes and releases on Git repositories. 9 | It provides simple, high-level commands to adopt the branching model describes in our documentation (see below). 10 | 11 | This tools is largely inspired by [GitFlow](https://github.com/nvie/gitflow), but the workflow is different. 12 | 13 | Feel free to contribute to it if you like. 14 | 15 | ## Requirements 16 | 17 | - Bash v4 _(2009)_ and above or zsh 18 | - Git v1.7.2 _(2010)_ and above 19 | - php5-cli or Python 2.x for Github, [Gitlab](https://about.gitlab.com), [Jira](https://www.atlassian.com/fr/wac/software/jira), 20 | [Redmine](http://www.redmine.org), [Trello](https://trello.com) and [Pivotal Tracker](https://www.pivotaltracker.com) connectors (can be switched off): allow to display issue's title/subject into twgit 21 | - Supported operating systems: Debian/Ubuntu Linux, FreeBSD, Mac OS X 22 | 23 | ## Installing twgit 24 | 25 | In the directory of your choice, e.g. `~/twgit`: 26 | 27 | ```bash 28 | $ git clone git@github.com:Twenga/twgit.git . 29 | $ sudo make install 30 | ``` 31 | 32 | More [Installation instructions](https://github.com/Twenga/twgit/wiki/Twgit#wiki-2.installation) are available in French wiki, waiting English translation... 33 | 34 | ## Getting started 35 | 36 | ![Getting started](doc/getting-started.png) 37 | 38 | ## Documentation 39 | 40 | [French documentation](https://github.com/Twenga/twgit/wiki) is available in wiki, waiting English translation... 41 | 42 | ### Help on command prompt 43 | 44 | ![twgit](doc/screenshot-twgit.png) 45 | 46 | ![twgit feature](doc/screenshot-twgit-feature.png) 47 | 48 | ![twgit hotfix](doc/screenshot-twgit-hotfix.png) 49 | 50 | ![twgit release](doc/screenshot-twgit-release.png) 51 | 52 | ![twgit demo](doc/screenshot-twgit-demo.png) 53 | 54 | ![twgit tag](doc/screenshot-twgit-tag.png) 55 | 56 | ## Continuous integration [![travis-ci.org](doc/travis-favicon.png)](http://travis-ci.org/Twenga/twgit) [![Build Status](https://secure.travis-ci.org/Twenga/twgit.png?branch=stable)](http://travis-ci.org/Twenga/twgit) 57 | 58 | To run the test suite locally, simply: 59 | 60 | ```bash 61 | $ cp conf/twgit-dist.sh conf/twgit.sh # and adapt, if necessary 62 | $ cp conf/phpunit-dist.php conf/phpunit.php # and adapt, if necessary 63 | $ phpunit -c conf/phpunit-dist.xml 64 | ``` 65 | 66 | To show estimated code coverage (_see_ [.travis.yml](.travis.yml)): 67 | 68 | ```bash 69 | $ tests/inc/codeCoverage.sh \ 70 | <(find $(pwd)/inc -type f -name '*.sh' && echo $(pwd)/twgit) \ 71 | <(find $(pwd) -type f -name '*Test.php') 72 | ``` 73 | 74 | [Estimated code coverage](https://travis-ci.org/Twenga/twgit): 33.1% (661 of 1994 lines). 75 | 76 | ## Copyrights & licensing 77 | 78 | Licensed under the Apache License 2.0. 79 | See [LICENSE](LICENSE) file for details. 80 | 81 | ## ChangeLog 82 | 83 | See [CHANGELOG](CHANGELOG.md) file for details. 84 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_redmine.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # Copyright (c) 2014 Laurent Toussaint 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2011 Twenga SA 22 | # @copyright 2012-2013 Geoffroy Aubry 23 | # @copyright 2014 Laurent Toussaint 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | 29 | ## 30 | # Retrieve and display subject of a Redmine's issue. 31 | # 32 | # @param string $1 issue number or project name 33 | # 34 | ref="$1" 35 | if [[ "$TWGIT_FEATURE_SUBJECT_REDMINE_DOMAIN" =~ ^https?:// ]]; then 36 | scheme='' 37 | else 38 | scheme='https://' 39 | fi 40 | issue_url="$scheme$TWGIT_FEATURE_SUBJECT_REDMINE_DOMAIN/issues/$ref.json?key=$TWGIT_FEATURE_SUBJECT_REDMINE_API_KEY" 41 | project_url="$scheme$TWGIT_FEATURE_SUBJECT_REDMINE_DOMAIN/projects/$ref.json?key=$TWGIT_FEATURE_SUBJECT_REDMINE_API_KEY" 42 | 43 | if ${has_wget}; then 44 | cmd="wget --no-check-certificate --timeout=3 -q -O - --no-cache" 45 | else 46 | cmd="curl --insecure --max-time 3 --silent -H \"Cache-control: no-cache\"" 47 | fi 48 | 49 | # Python or PHP ? 50 | language='?' 51 | which python 1>/dev/null 2>&1 52 | if [ $? -eq 0 ]; then 53 | language='python' 54 | else 55 | which php 1>/dev/null 2>&1 56 | if [ $? -eq 0 ]; then 57 | language='php' 58 | fi 59 | fi 60 | 61 | # Convert JSON with Python or PHP: 62 | if [ "$language" = 'python' ]; then 63 | if [[ "$ref" =~ ^[0-9]+$ ]]; then 64 | ($cmd $issue_url \ 65 | | python -c 'import sys,json;s=sys.stdin.read(); 66 | if s!="": data=json.loads(s); print data["issue"]["subject"].encode("utf8")') 67 | 2>/dev/null 68 | else 69 | ($cmd $project_url \ 70 | | python -c 'import sys,json;s=sys.stdin.read(); 71 | if s!="": data=json.loads(s); print data["project"]["name"].encode("utf8")') 72 | 2>/dev/null 73 | fi 74 | elif [ "$language" = 'php' ]; then 75 | if [[ "$ref" =~ ^[0-9]+$ ]]; then 76 | ($cmd $issue_url \ 77 | | php -r '$o = json_decode(file_get_contents("php://stdin")); if ($o !== NULL) {print_r($o->issue->subject);}') 78 | 2>/dev/null 79 | else 80 | ($cmd $project_url \ 81 | | php -r '$o = json_decode(file_get_contents("php://stdin")); if ($o !== NULL) {print_r($o->project->name);}') 82 | 2>/dev/null 83 | fi 84 | else 85 | echo "Language '$language' not handled!" >&2 86 | exit 1 87 | fi 88 | -------------------------------------------------------------------------------- /inc/compatibility.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # Copyright (c) 2012-2014 Laurent Toussaint 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2011 Twenga SA 22 | # @copyright 2012-2013 Geoffroy Aubry 23 | # @copyright 2012-2014 Laurent Toussaint 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | has_wget=false 29 | has_curl=false 30 | 31 | which wget 1>/dev/null 2>&1 32 | if [ $? -eq 0 ]; then 33 | has_wget=true 34 | fi 35 | 36 | which curl 1>/dev/null 2>&1 37 | if [ $? -eq 0 ]; then 38 | has_curl=true 39 | fi 40 | 41 | #-------------------------------------------------------------------- 42 | # Mac OS X compatibility layer 43 | #-------------------------------------------------------------------- 44 | 45 | # Witch OS: 46 | uname="$(uname)" 47 | if [ "$uname" = 'FreeBSD' ] || [ "$uname" = 'Darwin' ]; then 48 | TWGIT_OS='MacOSX' 49 | else 50 | TWGIT_OS='Linux' 51 | fi 52 | 53 | ## 54 | # Display the last update time of specified path, in seconds since 1970-01-01 00:00:00 UTC. 55 | # Compatible Linux and Mac OS X. 56 | # 57 | # @param string $1 path 58 | # @see $TWGIT_OS 59 | # 60 | function getLastUpdateTimestamp () { 61 | local path="$1" 62 | if [ "$TWGIT_OS" = 'MacOSX' ]; then 63 | stat -f %m "$path" 64 | else 65 | date -r "$path" +%s 66 | fi 67 | } 68 | 69 | ## 70 | # Display the specified timestamp converted to date with "+%Y-%m-%d %T" format. 71 | # Compatible Linux and Mac OS X. 72 | # 73 | # @param int $1 timestamp 74 | # @see $TWGIT_OS 75 | # 76 | function getDateFromTimestamp () { 77 | local timestamp="$1" 78 | if [ "$TWGIT_OS" = 'MacOSX' ]; then 79 | date -r "$timestamp" "+%Y-%m-%d %T" 2> /dev/null 80 | if [ $? -ne 0 ]; then 81 | date --date "1970-01-01 $timestamp sec" "+%Y-%m-%d %T" 82 | fi 83 | else 84 | date --date "1970-01-01 $timestamp sec" "+%Y-%m-%d %T" 85 | fi 86 | } 87 | 88 | ## 89 | # Execute sed with the specified regexp-extended pattern. 90 | # Compatible Linux and Mac OS X. 91 | # 92 | # @param string $1 pattern using extended regular expressions 93 | # @see $TWGIT_OS 94 | # 95 | function sedRegexpExtended () { 96 | local pattern="$1" 97 | if [ "$TWGIT_OS" = 'MacOSX' ]; then 98 | sed -E "$pattern"; 99 | else 100 | sed -r "$pattern"; 101 | fi 102 | } -------------------------------------------------------------------------------- /inc/twgit_tag.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # Copyright (c) 2013 Sebastien Hanicotte 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2011 Twenga SA 22 | # @copyright 2012-2013 Geoffroy Aubry 23 | # @copyright 2013 Sebastien Hanicotte 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | 29 | ## 30 | # Affiche l'aide de la commande tag. 31 | # 32 | # @testedby TwgitHelpTest 33 | # 34 | function usage () { 35 | echo; CUI_displayMsg help 'Usage:' 36 | CUI_displayMsg help_detail 'twgit tag ' 37 | echo; CUI_displayMsg help 'Available actions are:' 38 | CUI_displayMsg help_detail 'list [] [-F]' 39 | CUI_displayMsg help_detail ' List 5 last tags with included features. Add -F to do not make fetch.' 40 | CUI_displayMsg help_detail ' If is specified (using major.minor.revision format), then' 41 | CUI_displayMsg help_detail ' focus on this tag.'; echo 42 | CUI_displayMsg help_detail '[help]' 43 | CUI_displayMsg help_detail ' Display this help.'; echo 44 | } 45 | 46 | ## 47 | # Action déclenchant l'affichage de l'aide. 48 | # 49 | # @testedby TwgitHelpTest 50 | # 51 | function cmd_help () { 52 | usage; 53 | } 54 | 55 | ## 56 | # Liste les derniers tags ou l'un en particulier si spécifié. 57 | # Détaille les features incluses dans la release à la source du tag. 58 | # Gère l'option '-F' permettant d'éviter le fetch. 59 | # 60 | # @param string $1 nom court optionnel d'un tag 61 | # @testedby TwgitTagTest 62 | # 63 | function cmd_list () { 64 | process_options "$@" 65 | require_parameter '-' 66 | clean_prefixes "$RETVAL" 'tag' 67 | local tag="$RETVAL" 68 | local tag_fullname="$TWGIT_PREFIX_TAG$tag" 69 | process_fetch 'F' 70 | 71 | if [ ! -z "$tag" ]; then 72 | assert_valid_tag_name "$tag" 73 | ! has "$tag_fullname" $(get_all_tags) && die "Tag '$tag_fullname' does not exist! Try: twgit tag list" 74 | echo 75 | displayTag "$tag_fullname" 76 | echo 77 | else 78 | local max="$TWGIT_MAX_TAG_LIST_TO_SHOW" 79 | local tags=$(get_all_tags $max) 80 | CUI_displayMsg help "List $max last tags:" 81 | if [ -z "$tags" ]; then 82 | CUI_displayMsg info 'No tag exists.'; echo 83 | else 84 | for tag_fullname in $tags; do 85 | displayTag "$tag_fullname" 86 | echo 87 | done 88 | fi 89 | fi 90 | } 91 | -------------------------------------------------------------------------------- /tests/TwgitDyslexiaTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitDyslexiaTest extends TwgitTestCase 8 | { 9 | 10 | /** 11 | * @shcovers inc/dyslexia.inc.sh::guess_dyslexia 12 | * @dataProvider providerTestListAboutDyslexiaActionsAndCommands 13 | */ 14 | public function testList_AboutDyslexiaActionsAndCommands ($sScrambledCmd, $sExpectedContent, $sNotExpectedContent, $sRetval) 15 | { 16 | $sMsg = $this->_localFunctionCall('guess_dyslexia ' . $sScrambledCmd); 17 | if (strlen($sExpectedContent) > 0) { 18 | $this->assertContains($sExpectedContent, $sMsg); 19 | } 20 | if (strlen($sNotExpectedContent) > 0) { 21 | $this->assertNotContains($sNotExpectedContent, $sMsg); 22 | } 23 | 24 | $sCmd = 'guess_dyslexia ' . $sScrambledCmd . '; echo \$RETVAL;'; 25 | $sMsg = $this->_localShellCodeCall($sCmd); 26 | $this->assertContains($sRetval, $sMsg); 27 | } 28 | 29 | public function providerTestListAboutDyslexiaActionsAndCommands () 30 | { 31 | return array( 32 | array('lance', "Assume 'lance' was 'clean'…", '', 'clean'), 33 | /** Yep... But Lance <> wasn't so clean **/ 34 | array('mode', "Assume 'mode' was 'demo'…", '', 'demo'), 35 | array('itni', "Assume 'itni' was 'init'…", '', 'init'), 36 | /** Testing one clean command **/ 37 | array('clean', '', 'Assume', 'clean'), 38 | array('foxhit', "Assume 'foxhit' was 'hotfix'…", '', 'hotfix'), 39 | array('relasee', "Assume 'relasee' was 'release'…", '', 'release'), 40 | array('faeture', "Assume 'faeture' was 'feature'…", '', 'feature'), 41 | array('gta', "Assume 'gta' was 'tag'…", '', 'tag'), 42 | array('upadte', "Assume 'upadte' was 'update'…", '', 'update'), 43 | array('pehl', "Assume 'pehl' was 'help'…", '', 'help'), 44 | array('trats', "Assume 'trats' was 'start'…", '', 'start'), 45 | array('hsinif', "Assume 'hsinif' was 'finish'…", '', 'finish'), 46 | array('sautts', "Assume 'sautts' was 'status'…", '', 'status'), 47 | array('comimtetrs', "Assume 'comimtetrs' was 'committers'…", '', 'committers'), 48 | /** Testing one unknown hash key **/ 49 | array('commiters', '', 'Assume', 'commiters'), 50 | array('merg-einto-rleease', "Assume 'merg-einto-rleease' was 'merge-into-release'…", '', 'merge-into-release'), 51 | array('mereg-into-hoftix', "Assume 'mereg-into-hoftix' was 'merge-into-hotfix'…", '', 'merge-into-hotfix'), 52 | array('migarte', "Assume 'migarte' was 'migrate'…", '', 'migrate'), 53 | array('hups', "Assume 'hups' was 'push'…", '', 'push'), 54 | array('movere', "Assume 'movere' was 'remove'…", '', 'remove'), 55 | array('wath-changed', "Assume 'wath-changed' was 'what-changed'…", '', 'what-changed'), 56 | array('stil', "Assume 'stil' was 'list'…", '', 'list'), 57 | array('merge-faeture', "Assume 'merge-faeture' was 'merge-feature'…", '', 'merge-feature'), 58 | array('seter', "Assume 'seter' was 'reset'…", '', 'reset'), 59 | ); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /tests/TwgitOptionsHandlerTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitOptionsHandlerTest extends TwgitTestCase 8 | { 9 | 10 | /** 11 | * @dataProvider providerTestProcessOptions 12 | * @shcovers inc/options_handler.inc.sh::process_options 13 | */ 14 | public function testProcessOptions ($sIn, $sOutParameters, $sOutOptions) 15 | { 16 | $sCmd = 'process_options ' . $sIn . '; echo \$FCT_PARAMETERS; echo \$FCT_OPTIONS'; 17 | $sMsg = $this->_localShellCodeCall($sCmd); 18 | list($sParameters, $sOptions) = explode("\n", $sMsg); 19 | $this->assertEquals($sOutParameters, $sParameters); 20 | $this->assertEquals($sOutOptions, $sOptions); 21 | } 22 | 23 | public function providerTestProcessOptions () 24 | { 25 | return array( 26 | array('', '', ''), 27 | array('a', 'a', ''), 28 | array('-a', '', 'a'), 29 | array('x-a', 'x-a', ''), 30 | array('x -y Z', 'x Z', 'y'), 31 | array('x -yZ aa', 'x aa', 'y Z'), 32 | array('-x-yZ aa bbb c -rst', 'aa bbb c', 'x y Z r s t'), 33 | array('a -b C -d -e f', 'a C f', 'b d e'), 34 | ); 35 | } 36 | 37 | /** 38 | * @dataProvider providerTestIssetOption 39 | * @shcovers inc/options_handler.inc.sh::isset_option 40 | */ 41 | public function testIssetOption ($sInOptions, $sInOptionToTest, $sOut) 42 | { 43 | $sCmd = 'process_options ' . $sInOptions . '; isset_option ' . $sInOptionToTest . '; echo \$?'; 44 | $sMsg = $this->_localShellCodeCall($sCmd); 45 | $this->assertEquals($sOut, $sMsg); 46 | } 47 | 48 | public function providerTestIssetOption () 49 | { 50 | return array( 51 | array('a', 'a', '1'), 52 | array('a', 'b', '1'), 53 | array('-A', 'a', '1'), 54 | array('-a', 'A', '1'), 55 | array('-a', 'a', '0'), 56 | array('-A', 'A', '0'), 57 | array('x -aV', 'x', '1'), 58 | array('x -aV', 'X', '1'), 59 | array('x -aV', 'a', '0'), 60 | array('x -aV', 'V', '0'), 61 | ); 62 | } 63 | 64 | /** 65 | * @dataProvider providerTestSetOptions 66 | * @shcovers inc/options_handler.inc.sh::set_options 67 | */ 68 | public function testSetOptions ($sInOptions, $sInOptionsToAdd, $sInOptionToTest, $sOut) 69 | { 70 | $sCmd = 'process_options ' . $sInOptions . '; set_options ' . $sInOptionsToAdd 71 | . '; isset_option ' . $sInOptionToTest . '; echo \$?'; 72 | $sMsg = $this->_localShellCodeCall($sCmd); 73 | $this->assertEquals($sOut, $sMsg); 74 | } 75 | 76 | public function providerTestSetOptions () 77 | { 78 | return array( 79 | array('-a', 'a', 'a', '0'), 80 | array('-a', 'a', 'A', '1'), 81 | array('-a', 'A', 'a', '0'), 82 | array('-a', 'A', 'A', '0'), 83 | array('-a', 'a', 'b', '1'), 84 | array('-a', 'b', 'b', '0'), 85 | array('-a', 'b', 'c', '1'), 86 | array('-a', 'bc', 'b', '0'), 87 | array('-a', 'bc', 'c', '0'), 88 | array('-ab', 'bc', 'c', '0'), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /inc/connectors/feature_subject_gitlab.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2014 Karl Marques 9 | # Copyright (c) 2014 Laurent Toussaint 10 | # 11 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 12 | # with the License. You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 17 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 18 | # for the specific language governing permissions and limitations under the License. 19 | # 20 | # @copyright 2014 Karl Marques 21 | # @copyright 2014 Laurent Toussaint 22 | # @license http://www.apache.org/licenses/LICENSE-2.0 23 | # 24 | 25 | ## 26 | # Retrieve and display subject of a Gitlab's issue. 27 | # 28 | # @param string $1 issue number 29 | # 30 | 31 | ref="$1" 32 | project_addr=$(git config --get remote.$TWGIT_ORIGIN.url) 33 | 34 | gitlat_attribute="http_url_to_repo" 35 | 36 | if [[ $project_addr == *"ssh://"* ]]; then 37 | gitlat_attribute="ssh_url_to_repo" 38 | fi 39 | 40 | if [[ "$TWGIT_FEATURE_SUBJECT_GITLAB_DOMAIN" =~ ^https?:// ]]; then 41 | scheme='' 42 | else 43 | scheme='https://' 44 | fi 45 | 46 | project_url="$scheme$TWGIT_FEATURE_SUBJECT_GITLAB_DOMAIN/api/v3/projects/all?private_token=$TWGIT_FEATURE_SUBJECT_GITLAB_API_KEY" 47 | issue_url="$scheme$TWGIT_FEATURE_SUBJECT_GITLAB_DOMAIN/api/v3/projects/%s/issues?private_token=$TWGIT_FEATURE_SUBJECT_GITLAB_API_KEY" 48 | if ${has_wget}; then 49 | cmd="wget --no-check-certificate --timeout=3 -q -O - --no-cache" 50 | else 51 | cmd="curl --insecure --max-time 3 --silent -H \"Cache-control: no-cache\"" 52 | fi 53 | 54 | # Python or PHP ? 55 | language='?' 56 | which python 1>/dev/null 2>&1 57 | if [ $? -eq 0 ]; then 58 | language='python' 59 | else 60 | which php 1>/dev/null 2>&1 61 | if [ $? -eq 0 ]; then 62 | language='php' 63 | fi 64 | fi 65 | 66 | # Convert JSON with Python or PHP: 67 | if [ "$language" = 'python' ]; then 68 | 69 | if [[ "$ref" =~ ^[0-9]+$ ]]; then 70 | ($cmd $project_url \ 71 | | python -c 'import sys,json,urllib;s=sys.stdin.read(); 72 | data=json.loads(s) if s!="" else sys.exit(0) 73 | projectId=None 74 | for row in data: 75 | if row["'$gitlat_attribute'"] == "'$project_addr'" : 76 | projectId = row["id"]; break; 77 | f = urllib.urlopen("'$issue_url'" % (projectId)) 78 | s = f.read() 79 | data=json.loads(s) if s!="" else sys.exit(0); 80 | for row in data: 81 | if row["iid"] == '$ref' : 82 | print row["title"].encode("utf8"); break;') 83 | 2>/dev/null 84 | fi 85 | elif [ "$language" = 'php' ]; then 86 | 87 | if [[ "$ref" =~ ^[0-9]+$ ]]; then 88 | ($cmd $project_url \ 89 | | php -r '$o = json_decode(file_get_contents("php://stdin"));$projectid=array_reduce($o, function($carry, $item){if($carry==null && $item->'$gitlat_attribute' == "'$project_addr'"){return 90 | $item->id;}return $carry;}, null);$o=json_decode(file_get_contents(sprintf("'$issue_url'", $projectid,'$ref')));if ($o !== NULL) {array_walk($o, function($item, 91 | $key){if($item->iid == '$ref'){print_r($item->title);}});}') 92 | 2>/dev/null 93 | fi 94 | else 95 | echo "Language '$language' not handled!" >&2 96 | exit 1 97 | fi 98 | -------------------------------------------------------------------------------- /inc/options_handler.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Gestionnaire des paramètres et options (avec le tiret simple) des fonctions. 5 | # 6 | # Les options (une lettre max) peuvent être mélangées aux paramètres. 7 | # Syntaxe admises : 8 | # - 6 options dans cet exemple, les X sont des paramètres standards facultatifs : 9 | # [X] -a [X] -b-c [X] -def [X] 10 | # 11 | # Usage : 12 | # function f () { 13 | # process_options "$@" 14 | # isset_option 'f' && echo "OK" || echo "NOK" 15 | # require_parameter 'my_release' 16 | # local release="$RETVAL" 17 | # ... 18 | # } 19 | # 20 | # 21 | # 22 | # Copyright (c) 2011 Twenga SA 23 | # Copyright (c) 2012-2013 Geoffroy Aubry 24 | # 25 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 26 | # with the License. You may obtain a copy of the License at 27 | # 28 | # http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 31 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 32 | # for the specific language governing permissions and limitations under the License. 33 | # 34 | # @copyright 2011 Twenga SA 35 | # @copyright 2012-2013 Geoffroy Aubry 36 | # @license http://www.apache.org/licenses/LICENSE-2.0 37 | # 38 | 39 | 40 | 41 | # Globales du système de gestion : 42 | FCT_OPTIONS='' # concaténation des options sans les tirets, avec des espaces entre. 43 | FCT_PARAMETERS='' # concaténation des paramètres non options, avec des espaces entre. 44 | RETVAL='' # global to avoid subshell... 45 | 46 | ## 47 | # Analayse les paramètres et les répartis entre options et paramètres standards. 48 | # Les options sont précédées du préfixe '-' ou d'une option. 49 | # Rempli les tableaux globaux $FCT_OPTIONS et $FCT_PARAMETERS. 50 | # 51 | # @param string $@ liste de paramètres à analyser 52 | # @testedby TwgitOptionsHandlerTest 53 | # 54 | function process_options { 55 | local param 56 | while [ $# -gt 0 ]; do 57 | # PB pour récupérer la lettre option quand echo "-n"... 58 | # Du coup ceci ne fonctionne pas : param=`echo "$1" | grep -P '^-[^-]' | sed s/-//g` 59 | # Parade : 60 | [ ${#1} -gt 1 ] && [ ${1:0:1} = '-' ] && [ ${1:1:1} != '-' ] && param="${1:1}" || param='' 61 | 62 | param=$(echo "$param" | sed s/-//g) 63 | if [ ! -z "$param" ]; then 64 | FCT_OPTIONS="$FCT_OPTIONS $(echo $param | sed 's/\(.\)/\1 /g')" 65 | else 66 | FCT_PARAMETERS="$FCT_PARAMETERS $1" 67 | fi 68 | shift 69 | done 70 | FCT_PARAMETERS=${FCT_PARAMETERS:1} 71 | } 72 | 73 | ## 74 | # Est-ce que la valeur spécifiée fait partie de $FCT_OPTIONS ? 75 | # 76 | # @param string $1 valeur à rechercher, sans le préfixe '-' 77 | # @return 0 si présent, 1 sinon 78 | # @testedby TwgitOptionsHandlerTest 79 | # 80 | function isset_option () { 81 | local item=$1; shift 82 | echo " $FCT_OPTIONS " | grep -q " $(echo "$item" | sed 's/\([\.\+\$\*]\)/\\\1/g') " 83 | } 84 | 85 | ## 86 | # Ajoute les options spécifiées à $FCT_OPTIONS pour les considérer actives. 87 | # 88 | # @param string $1 options à ajouter, sans le préfixe '-' 89 | # @testedby TwgitOptionsHandlerTest 90 | # 91 | function set_options () { 92 | local items=$(echo $1); shift 93 | if [ ${#items} -ge 1 ]; then 94 | FCT_OPTIONS="$FCT_OPTIONS $(echo $items | sed 's/\(.\)/\1 /g')" 95 | fi 96 | } 97 | 98 | ## 99 | # Dépile le prochain paramètre de $FCT_PARAMETERS et le stock dans la globale $RETVAL. 100 | # 101 | # @param string $1 nom du paramètre demandé, qui servira pour le message d'erreur en cas de paramètre absent 102 | # Si le nom vaut '-' alors le paramètre est considéré comme optionnel. 103 | # 104 | function require_parameter () { 105 | local name=$1 106 | 107 | # On extrait le pâté de paramètres le plus à gauche : 108 | local param="${FCT_PARAMETERS%% *}" 109 | 110 | # On met à jour les paramètres restant à traiter : 111 | FCT_PARAMETERS="${FCT_PARAMETERS:$((${#param}+1))}" 112 | 113 | if [ ! -z "$param" ]; then 114 | RETVAL=$param 115 | elif [ "$name" = '-' ]; then 116 | RETVAL='' 117 | else 118 | CUI_displayMsg error "Missing argument <$name>!" 119 | usage 120 | exit 1 121 | fi 122 | } 123 | -------------------------------------------------------------------------------- /inc/coloredUI.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Provide an easy way to display colored and decorated messages in Bash: title, question, error, warning, success... 5 | # Just include this script, define colors, bold colors and headers, then call CUI_displayMsg() method. 6 | # 7 | # Generic example: 8 | # CUI_displayMsg type 'Message with bold section.' 9 | # `==> 'message with bold section.\033[0m' 10 | # 11 | # Concrete example: 12 | # . coloredUI.inc.sh 13 | # CUI_COLORS=( 14 | # [error]='\033[1;31m' 15 | # [help]='\033[0;36m' 16 | # [help.bold]='\033[1;36m' 17 | # [help.header]='\033[1;36m(i) ' 18 | # ) 19 | # CUI_displayMsg error 'Invalid number!' 20 | # `==> '\033[1;31mInvalid number!\033[0m' 21 | # CUI_displayMsg help 'This is a valuable information.' 22 | # `==> '\033[1;36m(i) \033[0;36mThis is a \033[1;36mvaluable\033[0;36m information.\033[0m' 23 | # 24 | # Requirements: 25 | # - Bash v4 (2009) and above 26 | # 27 | # Color codes : 28 | # - http://www.tux-planet.fr/les-codes-de-couleurs-en-bash/ 29 | # - http://confignewton.com/wp-content/uploads/2011/07/bash_color_codes.png 30 | # 31 | # 32 | # 33 | # Copyright (c) 2012-2013 Geoffroy Aubry 34 | # 35 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 36 | # with the License. You may obtain a copy of the License at 37 | # 38 | # http://www.apache.org/licenses/LICENSE-2.0 39 | # 40 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 41 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 42 | # for the specific language governing permissions and limitations under the License. 43 | # 44 | # @copyright 2012-2013 Geoffroy Aubry 45 | # @license http://www.apache.org/licenses/LICENSE-2.0 46 | # 47 | 48 | 49 | 50 | ## 51 | # Colors and decorations types. 52 | # 53 | # For each type, message will be displayed as follows (.header and .bold are optional): 54 | # 'message with bold section.\033[0m' 55 | # 56 | # For example: 57 | # CUI_COLORS=( 58 | # [type]='\033[1;31m' 59 | # [type.bold]='\033[1;33m' 60 | # [type.header]='\033[1m\033[4;33m/!\\\033[0;37m ' 61 | # ) 62 | # 63 | # @var associative array 64 | # 65 | declare -A CUI_COLORS 66 | 67 | ## 68 | # Check if the specified key exists in $CUI_COLORS associative array. 69 | # 70 | # @param string $1 key to check 71 | # @return int 0 if key exists, else 1 72 | # @testedby TwgitCUITest 73 | # 74 | function CUI_isSet () { 75 | local key="$1" 76 | [ -z "${CUI_COLORS[$key]-}" ] && return 1 || return 0 77 | } 78 | 79 | ## 80 | # Display a message of the specified type, using ${CUI_COLORS[$type]}. 81 | # If ${CUI_COLORS[$type.header]} exists, then this will be used as prefix. 82 | # If ${CUI_COLORS[$type.bold]} exists, then this will be used to display text in '...' tags. 83 | # In any case tags will be stripped. 84 | # 85 | # @param string $1 type of the message (error, title, ...) 86 | # @param string $2..$n message 87 | # @see $CUI_COLORS 88 | # @testedby TwgitCUITest 89 | # 90 | function CUI_displayMsg () { 91 | local type=$1; shift 92 | local msg="$*" 93 | local bold_pattern_start bold_pattern_end header 94 | 95 | # Color: 96 | if ! CUI_isSet "$type"; then 97 | echo "Unknown display type '$type'!" >&2 98 | echo -n 'Available types: ' >&2 99 | local types=$(echo "${!CUI_COLORS[*]}" | tr ' ' "\n" | grep -vE "\.bold$" | grep -vE "\.header$" | sort) 100 | local trimmed_types=$(echo $types) 101 | echo "${trimmed_types// /, }." >&2 102 | exit 1 103 | fi 104 | 105 | # Header: 106 | if ! CUI_isSet "$type.header"; then 107 | header='' 108 | else 109 | header="${CUI_COLORS[$type'.header']}" 110 | fi 111 | 112 | # Bold pattern: 113 | if ! CUI_isSet "$type.bold"; then 114 | bold_pattern_start='' 115 | bold_pattern_end='' 116 | else 117 | bold_pattern_start="${CUI_COLORS[$type'.bold']}" 118 | bold_pattern_end="${CUI_COLORS[$type]}" 119 | fi 120 | 121 | # Display: 122 | msg="${msg///$bold_pattern_start}" 123 | msg="${msg//<\/b>/$bold_pattern_end}" 124 | echo -e "$header${CUI_COLORS[$type]}$msg\033[0m" 125 | } 126 | -------------------------------------------------------------------------------- /tests/TwgitCommonToolsTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitCommonToolsTest extends TwgitTestCase 8 | { 9 | 10 | /** 11 | * @dataProvider providerTestDisplayQuotedEnum 12 | * @shcovers inc/common.inc.sh::displayQuotedEnum 13 | */ 14 | public function testDisplayQuotedEnum ($sValues, $sExpectedResult) 15 | { 16 | $sMsg = $this->_localFunctionCall('displayQuotedEnum "' . $sValues . '"'); 17 | $this->assertEquals($sExpectedResult, $sMsg); 18 | } 19 | 20 | public function providerTestDisplayQuotedEnum () 21 | { 22 | return array( 23 | array('', ''), 24 | array('a', "'a'"), 25 | array('a b', "'a', 'b'"), 26 | array(' a b ', "'a', 'b'"), 27 | array("a\nb", "'a', 'b'"), 28 | array("a \n b", "'a', 'b'"), 29 | array('a" "b', "'a', 'b'"), 30 | array('a b c', "'a', 'b', 'c'"), 31 | ); 32 | } 33 | 34 | /** 35 | * @dataProvider providerTestDisplayInterval 36 | * @shcovers inc/common.inc.sh::displayInterval 37 | */ 38 | public function testDisplayInterval ($sValues, $sExpectedResult) 39 | { 40 | $sMsg = $this->_localFunctionCall('displayInterval "' . $sValues . '"'); 41 | $this->assertEquals($sExpectedResult, $sMsg); 42 | } 43 | 44 | public function providerTestDisplayInterval () 45 | { 46 | return array( 47 | array('', ''), 48 | array('a', "'a'"), 49 | array('a b', "'a' to 'b'"), 50 | array(' a b ', "'a' to 'b'"), 51 | array("a\nb", "'a' to 'b'"), 52 | array("a \n b", "'a' to 'b'"), 53 | array('a" "b', "'a' to 'b'"), 54 | array('a b c', "'a' to 'c'"), 55 | ); 56 | } 57 | 58 | /** 59 | * @dataProvider providerConvertList2CSV 60 | * @shcovers inc/common.inc.sh::convertList2CSV 61 | */ 62 | public function testConvertList2CSV ($sValues, $sExpectedResult) 63 | { 64 | $sMsg = $this->_localFunctionCall('convertList2CSV ' . $sValues); 65 | $this->assertEquals($sExpectedResult, $sMsg); 66 | } 67 | 68 | public function providerConvertList2CSV () 69 | { 70 | return array( 71 | array('', ''), 72 | array('a', '"a"'), 73 | array('a b', '"a";"b"'), 74 | array('"a b"', '"a b"'), 75 | array(' a b ', '"a";"b"'), 76 | array('" a b "', '" a b "'), 77 | array("'a\nb'", '"a b"'), 78 | array('a b c', '"a";"b";"c"'), 79 | array('"a\"b"', '"a""b"'), 80 | array('"a\'b"', '"a\\\'b"'), 81 | ); 82 | } 83 | 84 | /** 85 | * @dataProvider providerCleanPrefixes 86 | * @shcovers inc/common.inc.sh::clean_prefixes 87 | */ 88 | public function testCleanPrefixes ($sBranchName, $sBranchType, $sExpectedResult) 89 | { 90 | $sMsg = $this->_localShellCodeCall('clean_prefixes ' . $sBranchName . ' ' . $sBranchType . '; echo \$RETVAL;'); 91 | $this->assertEquals($sExpectedResult, $sMsg); 92 | } 93 | 94 | public function providerCleanPrefixes () 95 | { 96 | return array( 97 | array('1224', 'feature', '1224'), 98 | array('7889', 'demo', '7889'), 99 | array('1.2.0', 'release', '1.2.0'), 100 | array('1.2.3', 'hotfix', '1.2.3'), 101 | array('3.1.4', 'tag', '3.1.4'), 102 | array('feature-1224', 'feature', "/!\\ Assume feature was '1224' instead of 'feature-1224'…\n1224"), 103 | array('demo-7889', 'demo', "/!\\ Assume demo was '7889' instead of 'demo-7889'…\n7889"), 104 | array('release-1.2.0', 'release', "/!\\ Assume release was '1.2.0' instead of 'release-1.2.0'…\n1.2.0"), 105 | array('hotfix-1.2.3', 'hotfix', "/!\\ Assume hotfix was '1.2.3' instead of 'hotfix-1.2.3'…\n1.2.3"), 106 | array('v3.1.4', 'tag', "/!\\ Assume tag was '3.1.4' instead of 'v3.1.4'…\n3.1.4"), 107 | array('tag-3.1.4', 'tag', 'tag-3.1.4'), 108 | array('unknown-148', 'unknown', 'unknown-148'), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/TwgitDemoTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitDemoTest extends TwgitTestCase 8 | { 9 | 10 | public function testStart_WithPrefixNaming () 11 | { 12 | $this->_remoteExec('git init'); 13 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 14 | $this->_localExec('git branch v1.2.3 v1.2.3'); 15 | 16 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo start demo-42'); 17 | $this->assertContains("Assume demo was '42' instead of 'demo-42'", $sMsg); 18 | } 19 | 20 | public function testList_WithPrefix () 21 | { 22 | $this->_remoteExec('git init'); 23 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 24 | $this->_localExec('git branch v1.2.3 v1.2.3'); 25 | $this->_localExec(TWGIT_EXEC . ' feature start 42'); 26 | $this->_localExec(TWGIT_EXEC . ' feature start 43'); 27 | $this->_localExec(TWGIT_EXEC . ' demo start 42'); 28 | $this->_localExec(TWGIT_EXEC . ' demo merge-feature 42'); 29 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo merge-feature feature-43'); 30 | $this->assertContains("Assume feature was '43' instead of 'feature-43'", $sMsg); 31 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo list demo-42'); 32 | $this->assertContains("Assume demo was '42' instead of 'demo-42'", $sMsg); 33 | $this->assertContains("feature-42", $sMsg); 34 | $this->assertContains("feature-43", $sMsg); 35 | } 36 | 37 | public function testRemove_WithPrefixes () 38 | { 39 | $this->_remoteExec('git init'); 40 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 41 | $this->_localExec(TWGIT_EXEC . ' demo start 42'); 42 | $this->_localExec('git checkout ' . self::STABLE); 43 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo remove demo-42'); 44 | $this->assertContains("Assume demo was '42' instead of 'demo-42'", $sMsg); 45 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo list'); 46 | $this->assertNotContains("demo-42", $sMsg); 47 | } 48 | 49 | public function testStatus_WithPrefixes () 50 | { 51 | $this->_remoteExec('git init'); 52 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 53 | $this->_localExec(TWGIT_EXEC . ' demo start 42'); 54 | $sMsg = $this->_localExec(TWGIT_EXEC . ' demo status demo-42'); 55 | $this->assertContains("Assume demo was '42' instead of 'demo-42'", $sMsg); 56 | } 57 | 58 | public function testStartFromDemo_WithoutRemote () 59 | { 60 | $this->_remoteExec('git init'); 61 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 62 | $this->setExpectedException('\RuntimeException', "Remote branch '" . self::_remote('demo-51') ."' not found!"); 63 | $this->_localExec(TWGIT_EXEC . ' demo start 42 from-demo 51'); 64 | } 65 | 66 | public function testStartFromDemo_WithExistingRemote () 67 | { 68 | $this->_remoteExec('git init'); 69 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 70 | $this->_remoteExec( 71 | 'git checkout -b demo-51' 72 | . ' && git commit --allow-empty -m "Initialize demo-51"' 73 | . ' && touch the-chosen-one' 74 | . ' && git add .' 75 | . ' && git commit -m "Add the chosen one"' 76 | ); 77 | $this->_localExec(TWGIT_EXEC . ' demo start 42 from-demo 51'); 78 | $sResult = $this->_localExec('ls'); 79 | $this->assertContains('the-chosen-one', $sResult); 80 | } 81 | 82 | public function testStartFromRelease_WithoutRemote () 83 | { 84 | $this->_remoteExec('git init'); 85 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 86 | $this->setExpectedException('\RuntimeException', 'No release in progress!'); 87 | $this->_localExec(TWGIT_EXEC . ' demo start 42 from-release'); 88 | } 89 | 90 | public function testStartFromRelease_WithExistingRemote () 91 | { 92 | $this->_remoteExec('git init'); 93 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 94 | $this->_remoteExec( 95 | 'git checkout -b release-1.3.0' 96 | . ' && git commit --allow-empty -m "Initialize release-1.3.0"' 97 | . ' && touch the-chosen-one' 98 | . ' && git add .' 99 | . ' && git commit -m "Add the chosen one"' 100 | ); 101 | $this->_localExec(TWGIT_EXEC . ' demo start 42 from-release'); 102 | $sResult = $this->_localExec('ls'); 103 | $this->assertContains('the-chosen-one', $sResult); 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /tests/inc/codeCoverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Usage: tests/inc/codeCoverage.sh 5 | # where is a file containing all Bash source filenames to analyze, 6 | # and is a file containing all PHP unit tests filenames. 7 | # 8 | # Example: tests/inc/codeCoverage.sh \ 9 | # <(find $(pwd)/inc -type f -name '*.sh' && echo $(pwd)/twgit) \ 10 | # <(find $(pwd) -type f -name '*Test.php') 11 | # 12 | # 13 | # 14 | # Copyright (c) 2012-2015 Geoffroy Aubry 15 | # Copyright (c) 2012-2014 Laurent Toussaint 16 | # 17 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 18 | # with the License. You may obtain a copy of the License at 19 | # 20 | # http://www.apache.org/licenses/LICENSE-2.0 21 | # 22 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 23 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 24 | # for the specific language governing permissions and limitations under the License. 25 | # 26 | # @copyright 2012-2015 Geoffroy Aubry 27 | # @copyright 2012-2014 Laurent Toussaint 28 | # @license http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | 31 | 32 | 33 | set -o nounset 34 | set -o pipefail 35 | 36 | . ./inc/compatibility.inc.sh 37 | 38 | src_file="$1" 39 | test_file="$2" 40 | rStats="/tmp/file.$$.$RANDOM" 41 | rCovers="/tmp/file.$$.$RANDOM" 42 | 43 | # Compute stats about Bash functions in a CSV file plus an extra line for total lines of code. 44 | # CSV format: path:function_name:start_line:end_line:nb_of_line_of_code 45 | # Use % instead of \000 in tr command, octal value doesn't work with sed command in mac os x 46 | # Skip functions with '# @codeCoverageIgnore'. 47 | cat "$src_file" \ 48 | | sort \ 49 | | xargs grep -E '^\s*#+\s*@codeCoverageIgnore\s*$|^\s*function\s+([a-z0-9_-]+)\b|^\}\s*$' \ 50 | --ignore-case --only-matching --line-number -r --with-filename \ 51 | | sedRegexpExtended 's#^./##' \ 52 | | awk -F: ' 53 | BEGIN {ignore=0; fct_begin=-1; fct_end=-1; fct_length=0} 54 | { 55 | if ($3 ~ /^\s*function/) { 56 | fct_name=substr($3, 10); fct_begin=$2; fct_end=fct_begin; fct_length=0 57 | } else if ($3 ~ /^}\s*$/) { 58 | if (ignore == 0) { 59 | fct_end=$2 60 | fct_length=fct_end-fct_begin-1 61 | print $1"\t"fct_name"\t"fct_begin"\t"fct_end"\t"fct_length 62 | } else { 63 | ignore=0 64 | } 65 | } else if ($3 ~ /@codeCoverageIgnore/) { 66 | ignore=1 67 | } 68 | }' \ 69 | | sort \ 70 | > $rStats 71 | 72 | # Find all @shcovers annotations in test code and store them in a file. 73 | # Format of annotation: @shcover path::function 74 | # Example: @shcovers inc/common.inc.sh::assert_git_configured 75 | cat "$test_file" \ 76 | | sort \ 77 | | xargs grep -E '^\s*\*\s*@shcovers\s+.+::.' --ignore-case -r --no-filename \ 78 | | tr -d '\r' \ 79 | | sedRegexpExtended 's/^.*@shcovers[ ]*//' \ 80 | | sedRegexpExtended 's/::/\t/' \ 81 | | sort | uniq \ 82 | > $rCovers 83 | 84 | # Compute coverage: 85 | iTotal="$(awk -F'\t' 'BEGIN {sum=0} {sum+=$5} END {print sum}' $rStats)" 86 | iSum="$(grep -f $rCovers $rStats | awk -F'\t' 'BEGIN {sum=0} {sum+=$5} END {print sum}')" 87 | (( p=iSum*1000/iTotal )) 88 | fPercent="${p:0:$((${#p}-1))}.${p:$((${#p}-1))}" 89 | 90 | # Example: "Estimated Bash code coverage: .4% (6 of 1334 lines)." 91 | echo -e "\n\033[1;33mEstimated Bash code coverage: \033[1;37m$fPercent%\033[0;33m ($iSum of $iTotal lines)." 92 | 93 | # Bash covered functions: 94 | echo -e "\n\033[1;32mBash covered functions:\033[0m" 95 | ( echo -e 'Script\tFunction\tStart line\tEnd line\tLOC'; grep -f $rCovers $rStats ) \ 96 | | column -t -s $'\t' \ 97 | | awk '{if (NR == 1) print "\033[1;37m" $0 "\033[0m"; else print $0}' \ 98 | | sedRegexpExtended 's/^/ /' 99 | 100 | # Bash uncovered functions: 101 | echo -e "\n\033[1;31mBash uncovered functions:\033[0m" 102 | if grep -f $rCovers -v $rStats -q; then 103 | ( echo -e 'Script\tFunction\tStart line\tEnd line\tLOC'; grep -f $rCovers -v $rStats ) \ 104 | | column -t -s $'\t' \ 105 | | awk '{if (NR == 1) print "\033[1;37m" $0 "\033[0m"; else print $0}' \ 106 | | sedRegexpExtended 's/^/ /' 107 | else 108 | echo ' All functions are covered.' 109 | fi 110 | 111 | # Example: "Estimated Bash code coverage: .4% (6 of 1334 lines)." 112 | echo -e "\n\033[1;33mEstimated Bash code coverage: \033[1;37m$fPercent%\033[0;33m ($iSum of $iTotal lines)." 113 | 114 | rm -f "$rStats" 115 | rm -f "$rCovers" 116 | -------------------------------------------------------------------------------- /makefile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SHARE_DIR="/usr/local/share/twgit" 4 | BIN_DIR="/usr/local/bin" 5 | ROOT_DIR=$(pwd) 6 | CONF_DIR="${ROOT_DIR}/conf" 7 | INSTALL_DIR="${ROOT_DIR}/install" 8 | 9 | USER_NAME=$(logname) 10 | USER_HOME=$(sudo -u ${USER_NAME} -H sh -c 'echo "$HOME"') 11 | 12 | CURRENT_SHELL=$(if [ ! -z $ZSH_NAME ]; then echo 'zsh'; else echo 'bash'; fi) 13 | CURRENT_SHELL_CMD=${SHELL} 14 | CURRENT_USER=${USER} 15 | CURRENT_BRANCH=$(git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g') 16 | CURRENT_OS=$(uname -s) 17 | 18 | if [ "${CURRENT_SHELL}" = "bash" ]; then 19 | if [ "${CURRENT_OS}" = "Darwin" ] || [ "${CURRENT_OS}" = "FreeBSD" ]; then 20 | BASH_RC="${USER_HOME}/.profile" 21 | else 22 | BASH_RC="${USER_HOME}/.${CURRENT_SHELL}rc" 23 | fi 24 | else 25 | BASH_RC="${USER_HOME}/.${CURRENT_SHELL}rc" 26 | fi 27 | 28 | # 29 | # Main method 30 | # 31 | main() { 32 | case $1 in 33 | install) 34 | check_stable 35 | check_root 36 | install 37 | ;; 38 | uninstall) 39 | check_root 40 | uninstall 41 | ;; 42 | help) 43 | help 44 | ;; 45 | *) 46 | echo "Uknown method '$1'" 47 | ;; 48 | esac 49 | } 50 | 51 | # 52 | # Check if current git branch is stable 53 | # 54 | check_stable() { 55 | if [ "${CURRENT_BRANCH}" != "stable" ]; then 56 | echo "You must be on 'stable' branch, not on '${CURRENT_BRANCH}' branch! Try: git checkout --track -b stable origin/stable" 57 | exit 2 58 | fi 59 | } 60 | 61 | # 62 | # Check if current user is root 63 | # 64 | check_root() { 65 | if [ "${CURRENT_USER}" != "root" ]; then 66 | echo "Sorry, you are not root." 67 | exit 1 68 | fi 69 | } 70 | 71 | # 72 | # Display help 73 | # 74 | help() { 75 | echo "Usage:" 76 | echo " sudo make install: to install Twgit in ${BIN_DIR}, ${CURRENT_SHELL} completion, config file and colored git prompt" 77 | echo " sudo make uninstall: to uninstall Twgit from ${BIN_DIR} and ${CURRENT_SHELL} completion" 78 | echo " make doc: to generate screenshots of help on command prompt" 79 | } 80 | 81 | # 82 | # Uninstall twgit 83 | # 84 | uninstall() { 85 | echo "" 86 | echo "1 - Remove executable" 87 | cmd="rm -f ${BIN_DIR}/twgit 2> /dev/null" 88 | echo '$> '$cmd 89 | eval $cmd 90 | 91 | opt="" 92 | for line in $(grep -in twgit ${BASH_RC} | cut -d: -f1); do 93 | opt="${opt} -e ${line}d " 94 | done 95 | if [ ! -z "${opt}" ]; then 96 | echo "" 97 | echo "2 - Remove completion" 98 | cmd="sed -i ${opt} ${BASH_RC}" 99 | echo '$> '$cmd 100 | eval $cmd 101 | fi 102 | return 0 103 | } 104 | 105 | # 106 | # Install twgit 107 | # 108 | install () { 109 | install_executable 110 | install_completion 111 | install_config 112 | install_prompt 113 | } 114 | 115 | # 116 | # Install executable 117 | # 118 | install_executable () { 119 | echo "" 120 | echo "1 - Install executable" 121 | echo "Check for previous install in '${BIN_DIR}/twgit'" 122 | if [ -f ${BIN_DIR}/twgit ]; then 123 | echo "Previous install found : clean" 124 | rm -f ${BIN_DIR}/twgit 125 | fi 126 | 127 | echo "Make twgit executable" 128 | ln -s ${ROOT_DIR}/twgit ${BIN_DIR}/twgit 129 | return 0 130 | } 131 | 132 | # 133 | # Install completion 134 | # 135 | install_completion () { 136 | echo "" 137 | echo "2 - Install completion" 138 | 139 | echo "Do you want to install the completion ? [Y/n]" 140 | read answer 141 | if [ "${answer}" != "Y" ] && [ "${answer}" != "y" ]; then 142 | echo "(i) Skip" 143 | return 0 144 | fi 145 | 146 | if [ $(cat ${BASH_RC} | grep -E "/${CURRENT_SHELL}_completion.sh" | grep -vE '^#' | wc -l) -gt 0 ]; then 147 | echo "Twgit Bash completion already loaded by '${BASH_RC}'." 148 | else 149 | echo "Add line '. ${INSTALL_DIR}/${CURRENT_SHELL}_completion.sh' at the bottom of the script '${BASH_RC}'." 150 | echo "" >> ${BASH_RC} 151 | echo "# Added by Twgit makefile:" >> ${BASH_RC} 152 | echo ". ${INSTALL_DIR}/${CURRENT_SHELL}_completion.sh" >> ${BASH_RC} 153 | echo "(i) Restart ${CURRENT_SHELL} session to enable configuration." 154 | fi 155 | } 156 | 157 | # 158 | # Install config 159 | # 160 | install_config () { 161 | echo "" 162 | echo "3 - Check Twgit config file:" 163 | if [ -f "${CONF_DIR}/twgit.sh" ]; then 164 | echo "Config file '${CONF_DIR}/twgit.sh' already existing." 165 | else 166 | echo "Copy config file from '${CONF_DIR}/twgit-dist.sh' to '${CONF_DIR}/twgit.sh'" 167 | sudo cp -p -n "${CONF_DIR}/twgit-dist.sh" "${CONF_DIR}/twgit.sh" 168 | fi 169 | } 170 | 171 | # 172 | # Install git prompt 173 | # 174 | install_prompt () { 175 | echo "" 176 | echo "4 - Install colored git prompt:" 177 | if [ $(cat ${BASH_RC} | grep -E "\.bash_twgit" | grep -vE '^#' | wc -l) -gt 0 ]; then 178 | echo "Colored Git prompt already loaded by '${BASH_RC}'." 179 | else 180 | echo "Add colored Git prompt to '${BASH_RC}' ? [Y/n] " 181 | read answer 182 | if [ "${answer}" = "Y" ] || [ "${answer}" = "y" ]; then 183 | echo "Install git prompt: ${USER_HOME}/.bash_twgit" 184 | sudo install -m 0644 -o ${SUDO_UID} -g ${SUDO_GID} "${INSTALL_DIR}/bash_git.sh" "${USER_HOME}/.bash_twgit" 185 | echo "Add line '. ~/.bash_twgit' at the end of the script '${BASH_RC}'." 186 | echo "" >> ${BASH_RC} 187 | echo "# Added by Twgit makefile:" >> ${BASH_RC} 188 | echo ". ~/.bash_twgit" >> ${BASH_RC} 189 | fi 190 | fi 191 | } 192 | 193 | 194 | # Run 195 | main "$@" 196 | -------------------------------------------------------------------------------- /conf/twgit-dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit config file 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # 11 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 12 | # with the License. You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 17 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 18 | # for the specific language governing permissions and limitations under the License. 19 | # 20 | # @copyright 2011 Twenga SA 21 | # @copyright 2012-2013 Geoffroy Aubry 22 | # @copyright 2014 Karl Marques 23 | # @license http://www.apache.org/licenses/LICENSE-2.0 24 | # 25 | 26 | 27 | 28 | TWGIT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 29 | TWGIT_INC_DIR="$TWGIT_ROOT_DIR/inc" 30 | TWGIT_CONF_DIR="$TWGIT_ROOT_DIR/conf" 31 | TWGIT_TMP_DIR="/tmp" 32 | 33 | TWGIT_BASH_EXEC="/bin/bash" 34 | TWGIT_EXEC="$TWGIT_BASH_EXEC $TWGIT_ROOT_DIR/twgit" 35 | 36 | # Name of file in root directory of each user repository to cache Redmine and Github feature's subject: 37 | TWGIT_FEATURES_SUBJECT_FILENAME='.twgit_features_subject' 38 | TWGIT_UPDATE_PATH="$TWGIT_ROOT_DIR/.lastupdate" 39 | TWGIT_UPDATE_NB_DAYS=2 40 | TWGIT_UPDATE_AUTO=1 # Laisser à 1 pour autoriser la MAJ auto. 41 | 42 | TWGIT_HISTORY_LOG_PATH="$TWGIT_ROOT_DIR/.history.log" 43 | TWGIT_HISTORY_ERROR_PATH="$TWGIT_ROOT_DIR/.history.error" 44 | TWGIT_HISTORY_SEPARATOR="----------------------------------------------------------------------\n[%s] %s\n" 45 | 46 | TWGIT_PREFIX_FEATURE='feature-' 47 | TWGIT_PREFIX_RELEASE='release-' 48 | TWGIT_PREFIX_HOTFIX='hotfix-' 49 | TWGIT_PREFIX_TAG='v' 50 | TWGIT_PREFIX_DEMO='demo-' 51 | 52 | TWGIT_ORIGIN='origin' 53 | TWGIT_STABLE='stable' 54 | 55 | TWGIT_PREFIX_COMMIT_MSG='[twgit] ' 56 | TWGIT_FIRST_COMMIT_MSG="${TWGIT_PREFIX_COMMIT_MSG}Init %s '%s'%s." # ie: [twgit] Init feature 'feature-1': full title. 57 | TWGIT_GIT_COMMAND_PROMPT='git# ' 58 | TWGIT_GIT_MIN_VERSION='1.7.2.0' 59 | 60 | TWGIT_DEFAULT_NB_COMMITTERS='3' 61 | 62 | # Default rendering option for twgit feature list, to choose in {'', 'c', 'd', 'x'}, 63 | # where 'c' stands for compact, 'd' for detailed and 'x' for eXtremely compact (CSV). 64 | TWGIT_FEATURE_LIST_DEFAULT_RENDERING_OPTION='' 65 | 66 | TWGIT_EMAIL_DOMAIN_NAME='' # e.g. twenga.com 67 | 68 | TWGIT_MAX_RETRIEVE_TAGS_NOT_MERGED=3 69 | TWGIT_MAX_TAG_LIST_TO_SHOW=5 70 | 71 | TWGIT_FEATURE_SUBJECT_CONNECTOR='' # in {'', 'github', 'redmine', 'jira', 'gitlab', 'pivotal'} 72 | TWGIT_FEATURE_SUBJECT_CONNECTOR_PATH="$TWGIT_INC_DIR/connectors/feature_subject_%s.sh" # où %s est un $TWGIT_FEATURE_SUBJECT_CONNECTOR 73 | TWGIT_FEATURE_SUBJECT_REDMINE_API_KEY='' # API key is a 40-byte hexadecimal string. 74 | TWGIT_FEATURE_SUBJECT_REDMINE_DOMAIN='' # e.g. 'www.redmine.org', with optionally scheme: 'http://', 'https://' (default). 75 | TWGIT_FEATURE_SUBJECT_GITLAB_API_KEY='' # API key 76 | TWGIT_FEATURE_SUBJECT_GITLAB_DOMAIN='' # e.g. 'www.gitlab.com' , with optionally scheme: 'http://', 'https://' (default). 77 | TWGIT_FEATURE_SUBJECT_GITHUB_USER='' # e.g. 'Twenga' 78 | TWGIT_FEATURE_SUBJECT_GITHUB_REPOSITORY='' # e.g. 'twgit' 79 | TWGIT_FEATURE_SUBJECT_JIRA_DOMAIN='' # e.g. 'www.abc.xyz' 80 | TWGIT_FEATURE_SUBJECT_JIRA_CREDENTIAL_BASE64='' # base64 (login:password) 81 | TWGIT_FEATURE_SUBJECT_TRELLO_DOMAIN='https://trello.com' # trello domain 82 | TWGIT_FEATURE_SUBJECT_TRELLO_APPLICATION_KEY='' # trello application key (check https://trello.com/docs/gettingstarted/index.html#getting-an-application-key) 83 | TWGIT_FEATURE_SUBJECT_TRELLO_TOKEN='' # trello token (check https://trello.com/docs/gettingstarted/index.html#getting-a-token-from-a-user) 84 | TWGIT_FEATURE_SUBJECT_PIVOTAL_PROJECT_ID='' # pivotal tracker project id 85 | TWGIT_FEATURE_SUBJECT_PIVOTAL_API_TOKEN='' # API key is a 40-byte hexadecimal string (check https://www.pivotaltracker.com/help/articles/api_token/) 86 | 87 | # All the files designed in the TWGIT_VERSION_INFO_PATH list will be 88 | # parsed in order to search for $Id$ or $Id:X.Y.Z$ and replace with 89 | # the current version on twgit init, twgit release start, and twgit hotfix start. 90 | # Example: being in v1.2.3 and calling twgit release start will result in replacing all tags with $Id:1.3.0$. 91 | # /!\ File list is comma separated: TWGIT_VERSION_INFO_PATH='file1.php,file2.php' 92 | TWGIT_VERSION_INFO_PATH='' 93 | 94 | ## 95 | # Colors and decorations types. 96 | # MUST define following types: 97 | # error, feature_subject, help, help_detail, info, normal, ok, processing, question, warning. 98 | # 99 | # For each type, message will be displayed as follows (.header and .bold are optional): 100 | # 'message with bold section.\033[0m' 101 | # 102 | # Color codes : 103 | # - http://www.tux-planet.fr/les-codes-de-couleurs-en-bash/ 104 | # - http://confignewton.com/wp-content/uploads/2011/07/bash_color_codes.png 105 | # 106 | # @var associative array 107 | # @see inc/coloredUI.inc.sh for more details. 108 | # 109 | declare -A CUI_COLORS=( 110 | [error]='\033[1;31m' 111 | [error.bold]='\033[1;33m' 112 | [error.header]='\033[1m\033[4;33m/!\\\033[0;37m ' 113 | [feature_subject]='\033[1;34m' 114 | [help]='\033[0;36m' 115 | [help.bold]='\033[1;36m' 116 | [help.header]='\033[1;36m(i) ' 117 | [help_detail]='\033[0;37m' 118 | [help_detail.bold]='\033[1;37m' 119 | [help_detail.header]=' ' 120 | [info]='\033[1;37m' 121 | [normal]='\033[0;37m' 122 | [ok]='\033[0;32m' 123 | [processing]='\033[1;30m' 124 | [question]='\033[1;33m' 125 | [question.bold]='\033[1;37m' 126 | [warning]='\033[0;33m' 127 | [warning.bold]='\033[1;33m' 128 | [warning.header]='\033[1m\033[4;33m/!\\\033[0;37m ' 129 | [current_branch]='\033[1;31m' 130 | ) 131 | -------------------------------------------------------------------------------- /tests/TwgitCommonGettersTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Geoffroy Letournel 7 | */ 8 | class TwgitCommonGettersTest extends TwgitTestCase 9 | { 10 | 11 | /** 12 | * @dataProvider providerTestGetDissidentRemoteBranches 13 | * @shcovers inc/common.inc.sh::get_dissident_remote_branches 14 | */ 15 | public function testGetDissidentRemoteBranches ($sLocalCmd, $sExpectedResult) 16 | { 17 | $this->_remoteExec('git init && git commit --allow-empty -m "-" && git checkout -b feature-currentOfNonBareRepo'); 18 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 19 | $this->_localExec('cd ' . TWGIT_REPOSITORY_SECOND_REMOTE_DIR . ' && git init'); 20 | $this->_localExec('git remote add second ' . TWGIT_REPOSITORY_SECOND_REMOTE_DIR); 21 | $this->_localExec('cd ' . TWGIT_REPOSITORY_THIRD_REMOTE_DIR . ' && git init'); 22 | $this->_localExec('git remote add third ' . TWGIT_REPOSITORY_THIRD_REMOTE_DIR); 23 | 24 | $this->_localExec($sLocalCmd); 25 | $sMsg = $this->_localFunctionCall('get_dissident_remote_branches'); 26 | $this->assertEquals($sExpectedResult, $sMsg); 27 | } 28 | 29 | public function providerTestGetDissidentRemoteBranches () 30 | { 31 | return array( 32 | array(':', ''), 33 | array( 34 | 'git checkout -b feature-X && git push ' . self::ORIGIN . ' feature-X' 35 | . ' && git checkout -b release-X && git push ' . self::ORIGIN . ' release-X' 36 | . ' && git checkout -b hotfix-X && git push ' . self::ORIGIN . ' hotfix-X' 37 | . ' && git checkout -b demo-X && git push ' . self::ORIGIN . ' demo-X' 38 | . ' && git checkout -b master && git push ' . self::ORIGIN . ' master' 39 | . ' && git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess' 40 | . ' && git remote set-head ' . self::ORIGIN . ' ' . self::STABLE, 41 | self::_remote('outofprocess') 42 | ), 43 | array( 44 | 'git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess && git push second outofprocess' 45 | . ' && git checkout -b out2 && git push ' . self::ORIGIN . ' out2 && git push second out2', 46 | self::_remote('out2') . "\n" . self::_remote('outofprocess') 47 | ), 48 | array( 49 | 'git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess && git push second outofprocess' 50 | . ' && git checkout -b out2 && git push ' . self::ORIGIN . ' out2 && git push third out2', 51 | self::_remote('out2') . "\n" . self::_remote('outofprocess') 52 | ), 53 | ); 54 | } 55 | 56 | /** 57 | * @shcovers inc/common.inc.sh::getFeatureSubject 58 | */ 59 | public function testGetFeatureSubject_WithNoParameter () 60 | { 61 | $sCmd = 'TWGIT_FEATURES_SUBJECT_PATH="$(mktemp ' . TWGIT_TMP_DIR . '/XXXXXXXXXX)"; ' 62 | . 'echo \'2;The subject of 2\' > \$TWGIT_FEATURES_SUBJECT_PATH; ' 63 | . 'config_file=\'F\'; TWGIT_FEATURE_SUBJECT_CONNECTOR=\'github\'; ' 64 | . 'getFeatureSubject; ' 65 | . 'rm -f "\$TWGIT_FEATURES_SUBJECT_PATH"'; 66 | $sMsg = $this->_localShellCodeCall($sCmd); 67 | $this->assertEmpty($sMsg); 68 | } 69 | 70 | /** 71 | * @shcovers inc/common.inc.sh::getFeatureSubject 72 | */ 73 | public function testGetFeatureSubject_WithParameterButNoSubjectNorConnector () 74 | { 75 | $sCmd = 'TWGIT_FEATURES_SUBJECT_PATH="$(mktemp ' . TWGIT_TMP_DIR . '/XXXXXXXXXX)"; ' 76 | . 'config_file=\'F\'; TWGIT_FEATURE_SUBJECT_CONNECTOR=\'no_connector\'; ' 77 | . 'getFeatureSubject 2; ' 78 | . 'rm -f "\$TWGIT_FEATURES_SUBJECT_PATH"'; 79 | $sMsg = $this->_localShellCodeCall($sCmd); 80 | $this->assertEmpty($sMsg); 81 | } 82 | 83 | /** 84 | * @shcovers inc/common.inc.sh::getFeatureSubject 85 | */ 86 | public function testGetFeatureSubject_WithParameterAndSubject () 87 | { 88 | $sCmd = 'TWGIT_FEATURES_SUBJECT_PATH="$(mktemp ' . TWGIT_TMP_DIR . '/XXXXXXXXXX)"; ' 89 | . 'echo \'2;The subject of 2\' > \$TWGIT_FEATURES_SUBJECT_PATH; ' 90 | . 'config_file=\'F\'; TWGIT_FEATURE_SUBJECT_CONNECTOR=\'no_connector\'; ' 91 | . 'getFeatureSubject 2; ' 92 | . 'rm -f "\$TWGIT_FEATURES_SUBJECT_PATH"'; 93 | $sMsg = $this->_localShellCodeCall($sCmd); 94 | $this->assertEquals('The subject of 2', $sMsg); 95 | } 96 | 97 | /** 98 | * @shcovers inc/common.inc.sh::getFeatureSubject 99 | */ 100 | // public function testGetFeatureSubject_WithParameterAndConnector () 101 | // { 102 | // $sCmd = 'TWGIT_FEATURES_SUBJECT_PATH="$(mktemp ' . TWGIT_TMP_DIR . '/XXXXXXXXXX)"; ' 103 | // . 'config_file=\'F\'; TWGIT_FEATURE_SUBJECT_CONNECTOR=\'github\'; ' 104 | // . 'getFeatureSubject 2; ' 105 | // . 'rm -f "\$TWGIT_FEATURES_SUBJECT_PATH"'; 106 | // $sMsg = $this->_localShellCodeCall($sCmd); 107 | // $this->assertEquals('email when too old features', $sMsg); 108 | // } 109 | // => Pb with API rate limit: http://developer.github.com/v3/#rate-limiting 110 | 111 | /** 112 | * @shcovers inc/common.inc.sh::displayFeatureSubject 113 | */ 114 | public function testDisplayFeatureSubject_WithKnownFeature () 115 | { 116 | $sCmd = 'function getFeatureSubject() { echo "XYZ-\$1";}; ' 117 | . 'displayFeatureSubject 2'; 118 | $sMsg = $this->_localShellCodeCall($sCmd); 119 | $this->assertEquals('XYZ-2', $sMsg); 120 | } 121 | 122 | /** 123 | * @shcovers inc/common.inc.sh::displayFeatureSubject 124 | */ 125 | public function testDisplayFeatureSubject_WithUnknownFeature () 126 | { 127 | $sCmd = 'function getFeatureSubject() { echo ;}; ' 128 | . 'displayFeatureSubject 2 \"default subject\"'; 129 | $sMsg = $this->_localShellCodeCall($sCmd); 130 | $this->assertEquals('default subject', $sMsg); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/TwgitTagTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Geoffroy Letournel 7 | */ 8 | class TwgitTagTest extends TwgitTestCase 9 | { 10 | 11 | /** 12 | * @shcovers inc/twgit_tag.inc.sh::cmd_list 13 | */ 14 | public function testList_WithAmbiguousRef () 15 | { 16 | $this->_remoteExec('git init'); 17 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 18 | $this->_localExec('git branch v1.2.3 v1.2.3'); 19 | 20 | $sMsg = $this->_localExec(TWGIT_EXEC . ' tag list'); 21 | $this->assertNotContains("warning: refname 'v1.2.3' is ambiguous.", $sMsg); 22 | } 23 | 24 | /** 25 | * @shcovers inc/twgit_tag.inc.sh::cmd_list 26 | */ 27 | public function testList_WithUnknownRef () 28 | { 29 | $this->_remoteExec('git init'); 30 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 31 | $this->_localExec('git branch v1.2.3 v1.2.3'); 32 | 33 | $this->setExpectedException('RuntimeException', "/!\ Tag 'v6.6.6' does not exist! Try: twgit tag list"); 34 | $this->_localExec(TWGIT_EXEC . ' tag list 6.6.6'); 35 | } 36 | 37 | /** 38 | * @shcovers inc/twgit_tag.inc.sh::cmd_list 39 | */ 40 | public function testList_WithBadRef () 41 | { 42 | $this->_remoteExec('git init'); 43 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 44 | $this->_localExec('git branch v1.2.3 v1.2.3'); 45 | 46 | $this->setExpectedException( 47 | 'RuntimeException', 48 | "/!\ Unauthorized tag name: 'toto'! Must use format, e.g. '1.2.3'." 49 | ); 50 | $sMsg = $this->_localExec(TWGIT_EXEC . ' tag list toto'); 51 | } 52 | 53 | /** 54 | * @medium 55 | * @dataProvider providerTestListWithValidTag 56 | * @shcovers inc/twgit_tag.inc.sh::cmd_list 57 | */ 58 | public function testList_WithValidTag ($sSubCmd, $sExpectedResult) 59 | { 60 | $this->_remoteExec('git init'); 61 | $this->_localExec( 62 | "git init && \\ 63 | git config user.name 'Firstname Lastname' && \\ 64 | git config user.email 'firstname.lastname@xyz.com'" 65 | ); 66 | $this->_localExec('git add .twgit && git commit -am init'); 67 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 68 | $this->_localExec(TWGIT_EXEC . ' release start -I'); 69 | 70 | $this->_localExec('echo \'2;The subject of 2\' > .twgit_features_subject'); 71 | $this->_localExec('echo \'4;The subject of 4\' >> .twgit_features_subject'); 72 | $this->_localExec(TWGIT_EXEC . ' feature start 1'); 73 | $this->_localExec(TWGIT_EXEC . ' feature start 2'); 74 | $this->_localExec('git merge --no-ff feature-1; git commit --allow-empty -m "empty"; git push ' . self::ORIGIN . ';'); 75 | $this->_localExec(TWGIT_EXEC . ' feature start 3'); 76 | $this->_localExec(TWGIT_EXEC . ' feature start 4'); 77 | $this->_localExec(TWGIT_EXEC . ' feature start 5'); 78 | 79 | $this->_localExec(TWGIT_EXEC . ' feature merge-into-release 2'); 80 | $this->_localExec(TWGIT_EXEC . ' feature merge-into-release 4'); 81 | $this->_localExec(TWGIT_EXEC . ' feature merge-into-release 5'); 82 | $this->_localExec(TWGIT_EXEC . ' release finish -I'); 83 | 84 | $this->_localExec('echo \'1;The NEW subject of 1\' > .twgit_features_subject'); 85 | $this->_localExec('echo \'2;The NEW subject of 2\' >> .twgit_features_subject'); 86 | $sMsg = $this->_localExec(TWGIT_EXEC . ' tag list' . $sSubCmd); 87 | $sMsg = preg_replace("/^Date:.*$/mi", 'Date: ---', $sMsg); 88 | $this->assertContains($sExpectedResult, $sMsg); 89 | } 90 | 91 | public function providerTestListWithValidTag () { 92 | return array( 93 | array( 94 | '', "git# git fetch --prune " . self::ORIGIN 95 | . "\n" 96 | . "\n(i) List 5 last tags:" 97 | . "\nTag: v1.2.3" 98 | . "\nTagger: Firstname Lastname " 99 | . "\nDate: ---" 100 | . "\nNo feature included and it's the first tag." 101 | . "\n" 102 | . "\nTag: v1.3.0" 103 | . "\nTagger: Firstname Lastname " 104 | . "\nDate: ---" 105 | . "\nIncluded features:" 106 | . "\n - " . self::ORIGIN . "/feature-5" 107 | . "\n - " . self::ORIGIN . "/feature-4 The subject of 4" 108 | . "\n - " . self::ORIGIN . "/feature-2 The NEW subject of 2" 109 | . "\n - " . self::ORIGIN . "/feature-1 The NEW subject of 1" 110 | . "\n" 111 | ), 112 | array( 113 | ' 1.2.3', "git# git fetch --prune " . self::ORIGIN 114 | . "\n" 115 | . "\nCheck valid ref name..." 116 | . "\nCheck valid tag name..." 117 | . "\n" 118 | . "\nTag: v1.2.3" 119 | . "\nTagger: Firstname Lastname " 120 | . "\nDate: ---" 121 | . "\nNo feature included and it's the first tag." 122 | . "\n" 123 | ), 124 | array( 125 | ' 1.3.0', "git# git fetch --prune " . self::ORIGIN 126 | . "\n" 127 | . "\nCheck valid ref name..." 128 | . "\nCheck valid tag name..." 129 | . "\n" 130 | . "\nTag: v1.3.0" 131 | . "\nTagger: Firstname Lastname " 132 | . "\nDate: ---" 133 | . "\nIncluded features:" 134 | . "\n - " . self::ORIGIN . "/feature-5" 135 | . "\n - " . self::ORIGIN . "/feature-4 The subject of 4" 136 | . "\n - " . self::ORIGIN . "/feature-2 The NEW subject of 2" 137 | . "\n - " . self::ORIGIN . "/feature-1 The NEW subject of 1" 138 | . "\n" 139 | ), 140 | array( 141 | ' v1.3.0', "Assume tag was '1.3.0' instead of 'v1.3.0'" 142 | ), 143 | ); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /tests/TwgitCommonProcessingTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Geoffroy Letournel 7 | * @author Sebastien Hanicotte 8 | */ 9 | class TwgitCommonProcessingTest extends TwgitTestCase 10 | { 11 | 12 | /** 13 | * Sets up the fixture, for example, open a network connection. 14 | * This method is called before a test is executed. 15 | */ 16 | public function setUp () 17 | { 18 | parent::setUp(); 19 | $this->_remoteExec('git init'); 20 | $this->_localExec(TWGIT_EXEC . ' init 1.0.0 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 21 | } 22 | 23 | /** 24 | * @shcovers inc/common.inc.sh::exec_git_command 25 | */ 26 | public function testExecGitCommand_WithoutError () 27 | { 28 | $sMsg = $this->_localFunctionCall('exec_git_command "git --version" "error msg"'); 29 | $this->assertContains("git# git --version\ngit version ", $sMsg); 30 | } 31 | 32 | /** 33 | * @shcovers inc/common.inc.sh::exec_git_command 34 | */ 35 | public function testExecGitCommand_ThrowExceptionWhenError () 36 | { 37 | $this->setExpectedException('RuntimeException', "/!\ error msg"); 38 | $sMsg = $this->_localFunctionCall('exec_git_command "git checkout notexists" "error msg"'); 39 | } 40 | 41 | /** 42 | * @shcovers inc/common.inc.sh::remove_local_branch 43 | */ 44 | public function testRemoveLocalBranch_WhenNotExists () 45 | { 46 | $sMsg = $this->_localFunctionCall('remove_local_branch notexists'); 47 | $this->assertEquals("Local branch 'notexists' not found.", $sMsg); 48 | } 49 | 50 | /** 51 | * @shcovers inc/common.inc.sh::remove_local_branch 52 | */ 53 | public function testRemoveLocalBranch_WhenExistsAndNotCurrent () 54 | { 55 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2'); 56 | $this->_localFunctionCall('remove_local_branch feature-1'); 57 | $sMsg = $this->_localExec("git branch -a | sed 's/^[* ]*//' | sed 's/ *$//g'"); 58 | $this->assertEquals( 59 | "feature-2\n" . 60 | self::STABLE . "\n" . 61 | implode("\n", self::_remotes(array('feature-1', 'feature-2', self::STABLE))), 62 | $sMsg 63 | ); 64 | } 65 | 66 | /** 67 | * @shcovers inc/common.inc.sh::remove_remote_branch 68 | */ 69 | public function testRemoveRemoteBranch_ThrowExceptionWhenNotExists () 70 | { 71 | $this->setExpectedException('RuntimeException', "/!\ Remote branch '" . self::ORIGIN . "/notexists' not found!"); 72 | $sMsg = $this->_localFunctionCall('remove_remote_branch notexists'); 73 | } 74 | 75 | /** 76 | * @shcovers inc/common.inc.sh::remove_remote_branch 77 | */ 78 | public function testRemoveRemoteBranch_WhenExistsAndNotAlreadyDeleted () 79 | { 80 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2; '); 81 | $this->_localFunctionCall('remove_remote_branch feature-1'); 82 | $sMsg = $this->_localExec("git branch -a | sed 's/^[* ]*//' | sed 's/ *$//g'"); 83 | $this->assertEquals( 84 | "feature-1\n" . 85 | "feature-2\n" . 86 | self::STABLE . "\n" . 87 | implode("\n", self::_remotes(array('feature-2', self::STABLE))), 88 | $sMsg 89 | ); 90 | } 91 | 92 | /** 93 | * @shcovers inc/common.inc.sh::remove_remote_branch 94 | */ 95 | public function testRemoveRemoteBranch_ThrowExceptionWhenAlreadyDeleted () 96 | { 97 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2; '); 98 | $this->_localExec( 99 | 'git init && git remote add ' . self::ORIGIN . ' ' . TWGIT_REPOSITORY_ORIGIN_DIR . ' && git fetch ' . self::ORIGIN 100 | , true, 2 101 | ); 102 | $this->_localFunctionCall('remove_remote_branch feature-1'); 103 | 104 | $this->setExpectedException('RuntimeException', "/!\ Delete remote branch '" . self::ORIGIN . "/feature-1' failed!"); 105 | $sMsg = $this->_localFunctionCall('remove_remote_branch feature-1', true, 2); 106 | } 107 | 108 | /** 109 | * @shcovers inc/common.inc.sh::remove_branch 110 | */ 111 | public function testRemoveBranch () 112 | { 113 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2; '); 114 | $sMsg = $this->_localFunctionCall('remove_branch 1 feature-'); 115 | $this->assertContains('Check valid ref name...', $sMsg); 116 | $this->assertContains('Check clean working tree...', $sMsg); 117 | $this->assertContains('Check current branch...', $sMsg); 118 | $this->assertContains('git# git fetch --prune ' . self::ORIGIN, $sMsg); 119 | 120 | $sMsg = $this->_localExec("git branch -a | sed 's/^[* ]*//' | sed 's/ *$//g'"); 121 | $this->assertEquals( 122 | "feature-2\n" . 123 | self::STABLE . "\n" . 124 | implode("\n", self::_remotes(array('feature-2', self::STABLE))), 125 | $sMsg 126 | ); 127 | } 128 | 129 | /** 130 | * @shcovers inc/common.inc.sh::remove_demo 131 | */ 132 | public function testRemoveDemo () 133 | { 134 | $this->_localExec(TWGIT_EXEC . ' demo start 1; ' . TWGIT_EXEC . ' demo start 2; '); 135 | $sMsg = $this->_localFunctionCall('remove_demo 1'); 136 | $this->assertContains('Check valid ref name...', $sMsg); 137 | $this->assertContains('Check clean working tree...', $sMsg); 138 | $this->assertContains('Check current branch...', $sMsg); 139 | $this->assertContains('git# git fetch --prune ' . self::ORIGIN, $sMsg); 140 | 141 | $sMsg = $this->_localExec("git branch -a | sed 's/^[* ]*//' | sed 's/ *$//g'"); 142 | $this->assertEquals( 143 | "demo-2\n" . 144 | self::STABLE . "\n" . 145 | implode("\n", self::_remotes(array('demo-2', self::STABLE))), 146 | $sMsg 147 | ); 148 | } 149 | 150 | /** 151 | * @shcovers inc/common.inc.sh::remove_feature 152 | */ 153 | public function testRemoveFeature () 154 | { 155 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2; '); 156 | $sMsg = $this->_localFunctionCall('remove_feature 1'); 157 | $this->assertContains('Check valid ref name...', $sMsg); 158 | $this->assertContains('Check clean working tree...', $sMsg); 159 | $this->assertContains('Check current branch...', $sMsg); 160 | $this->assertContains('git# git fetch --prune ' . self::ORIGIN, $sMsg); 161 | 162 | $sMsg = $this->_localExec("git branch -a | sed 's/^[* ]*//' | sed 's/ *$//g'"); 163 | $this->assertEquals( 164 | "feature-2\n" . 165 | self::STABLE . "\n" . 166 | implode("\n", self::_remotes(array('feature-2', self::STABLE))), 167 | $sMsg 168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/TwgitHelpTest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitHelpTest extends TwgitTestCase 8 | { 9 | 10 | /** 11 | * Sets up the fixture, for example, open a network connection. 12 | * This method is called before a test is executed. 13 | */ 14 | public function setUp () 15 | { 16 | } 17 | 18 | /** 19 | * @shcovers twgit::usage 20 | */ 21 | public function testMainHelp_ThrowExcpetionWhenUnknownAction1 () 22 | { 23 | $this->setExpectedException('RuntimeException', "Command not found: 'unknownaction'"); 24 | $this->_exec(TWGIT_EXEC . ' unknownaction'); 25 | } 26 | 27 | /** 28 | * @shcovers twgit::usage 29 | */ 30 | public function testMainHelp_ThrowExcpetionWhenUnknownAction2 () 31 | { 32 | $this->setExpectedException('RuntimeException', "Usage:\n twgit []"); 33 | $this->_exec(TWGIT_EXEC . ' unknownaction'); 34 | } 35 | 36 | /** 37 | * @shcovers twgit::usage 38 | * @shcovers twgit::cmd_help 39 | */ 40 | public function testMainHelp_OnHelpAction () 41 | { 42 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' help'); 43 | $this->assertContains("Usage:\n twgit []", $sMsg); 44 | } 45 | 46 | /** 47 | * @shcovers twgit::usage 48 | * @shcovers twgit::cmd_help 49 | */ 50 | public function testMainHelp_WithNoAction () 51 | { 52 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC); 53 | $this->assertContains("Usage:\n twgit []", $sMsg); 54 | } 55 | 56 | /** 57 | * @shcovers inc/twgit_feature.inc.sh::usage 58 | */ 59 | public function testFeatureHelp_ThrowExcpetionWhenUnknownAction1 () 60 | { 61 | $this->setExpectedException('RuntimeException', "Unknown action: 'unknownaction'"); 62 | $this->_exec(TWGIT_EXEC . ' feature unknownaction'); 63 | } 64 | 65 | /** 66 | * @shcovers inc/twgit_feature.inc.sh::usage 67 | */ 68 | public function testFeatureHelp_ThrowExcpetionWhenUnknownAction2 () 69 | { 70 | $this->setExpectedException('RuntimeException', "Usage:\n twgit feature "); 71 | $this->_exec(TWGIT_EXEC . ' feature unknownaction'); 72 | } 73 | 74 | /** 75 | * @shcovers inc/twgit_feature.inc.sh::usage 76 | * @shcovers inc/twgit_feature.inc.sh::cmd_help 77 | */ 78 | public function testFeatureHelp_OnHelpAction () 79 | { 80 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' feature help'); 81 | $this->assertContains("Usage:\n twgit feature ", $sMsg); 82 | } 83 | 84 | /** 85 | * @shcovers inc/twgit_feature.inc.sh::usage 86 | * @shcovers inc/twgit_feature.inc.sh::cmd_help 87 | */ 88 | public function testFeatureHelp_WithNoAction () 89 | { 90 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' feature'); 91 | $this->assertContains("Usage:\n twgit feature ", $sMsg); 92 | } 93 | 94 | /** 95 | * @shcovers inc/twgit_release.inc.sh::usage 96 | */ 97 | public function testReleaseHelp_ThrowExcpetionWhenUnknownAction1 () 98 | { 99 | $this->setExpectedException('RuntimeException', "Unknown action: 'unknownaction'"); 100 | $this->_exec(TWGIT_EXEC . ' release unknownaction'); 101 | } 102 | 103 | /** 104 | * @shcovers inc/twgit_release.inc.sh::usage 105 | */ 106 | public function testReleaseHelp_ThrowExcpetionWhenUnknownAction2 () 107 | { 108 | $this->setExpectedException('RuntimeException', "Usage:\n twgit release "); 109 | $this->_exec(TWGIT_EXEC . ' release unknownaction'); 110 | } 111 | 112 | /** 113 | * @shcovers inc/twgit_release.inc.sh::usage 114 | * @shcovers inc/twgit_release.inc.sh::cmd_help 115 | */ 116 | public function testReleaseHelp_OnHelpAction () 117 | { 118 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' release help'); 119 | $this->assertContains("Usage:\n twgit release ", $sMsg); 120 | } 121 | 122 | /** 123 | * @shcovers inc/twgit_release.inc.sh::usage 124 | * @shcovers inc/twgit_release.inc.sh::cmd_help 125 | */ 126 | public function testReleaseHelp_WithNoAction () 127 | { 128 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' release'); 129 | $this->assertContains("Usage:\n twgit release ", $sMsg); 130 | } 131 | 132 | /** 133 | * @shcovers inc/twgit_hotfix.inc.sh::usage 134 | */ 135 | public function testHotfixHelp_ThrowExcpetionWhenUnknownAction1 () 136 | { 137 | $this->setExpectedException('RuntimeException', "Unknown action: 'unknownaction'"); 138 | $this->_exec(TWGIT_EXEC . ' hotfix unknownaction'); 139 | } 140 | 141 | /** 142 | * @shcovers inc/twgit_hotfix.inc.sh::usage 143 | */ 144 | public function testHotfixHelp_ThrowExcpetionWhenUnknownAction2 () 145 | { 146 | $this->setExpectedException('RuntimeException', "Usage:\n twgit hotfix "); 147 | $this->_exec(TWGIT_EXEC . ' hotfix unknownaction'); 148 | } 149 | 150 | /** 151 | * @shcovers inc/twgit_hotfix.inc.sh::usage 152 | * @shcovers inc/twgit_hotfix.inc.sh::cmd_help 153 | */ 154 | public function testHotfixHelp_OnHelpAction () 155 | { 156 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' hotfix help'); 157 | $this->assertContains("Usage:\n twgit hotfix ", $sMsg); 158 | } 159 | 160 | /** 161 | * @shcovers inc/twgit_hotfix.inc.sh::usage 162 | * @shcovers inc/twgit_hotfix.inc.sh::cmd_help 163 | */ 164 | public function testHotfixHelp_WithNoAction () 165 | { 166 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' hotfix'); 167 | $this->assertContains("Usage:\n twgit hotfix ", $sMsg); 168 | } 169 | 170 | /** 171 | * @shcovers inc/twgit_tag.inc.sh::usage 172 | */ 173 | public function testTagHelp_ThrowExcpetionWhenUnknownAction1 () 174 | { 175 | $this->setExpectedException('RuntimeException', "Unknown action: 'unknownaction'"); 176 | $this->_exec(TWGIT_EXEC . ' tag unknownaction'); 177 | } 178 | 179 | /** 180 | * @shcovers inc/twgit_tag.inc.sh::usage 181 | */ 182 | public function testTagHelp_ThrowExcpetionWhenUnknownAction2 () 183 | { 184 | $this->setExpectedException('RuntimeException', "Usage:\n twgit tag "); 185 | $this->_exec(TWGIT_EXEC . ' tag unknownaction'); 186 | } 187 | 188 | /** 189 | * @shcovers inc/twgit_tag.inc.sh::usage 190 | * @shcovers inc/twgit_tag.inc.sh::cmd_help 191 | */ 192 | public function testTagHelp_OnHelpAction () 193 | { 194 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' tag help'); 195 | $this->assertContains("Usage:\n twgit tag ", $sMsg); 196 | } 197 | 198 | /** 199 | * @shcovers inc/twgit_tag.inc.sh::usage 200 | * @shcovers inc/twgit_tag.inc.sh::cmd_help 201 | */ 202 | public function testTagHelp_WithNoAction () 203 | { 204 | $sMsg = $this->_exec('cd ' . TWGIT_TMP_DIR . '; ' . TWGIT_EXEC . ' tag'); 205 | $this->assertContains("Usage:\n twgit tag ", $sMsg); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /tests/TwgitCUITest.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class TwgitCUITest extends TwgitTestCase 8 | { 9 | 10 | /** 11 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 12 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 13 | */ 14 | public function testDisplayMsg_ThrowExceptionWhenUnknownTypeAndNoDefinedType () 15 | { 16 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: ."); 17 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=(); CUI_displayMsg info', false); 18 | } 19 | 20 | /** 21 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 22 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 23 | */ 24 | public function testDisplayMsg_ThrowExceptionWhenUnknownTypeAndOneDefinedType () 25 | { 26 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: a."); 27 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([a]=b); CUI_displayMsg info', false); 28 | } 29 | 30 | /** 31 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 32 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 33 | */ 34 | public function testDisplayMsg_ThrowExceptionWhenUnknownTypeAndSeveralDefinedTypes () 35 | { 36 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: a, c."); 37 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([a]=b [c]=d [c.bold]=d2 [c.header]=d3); CUI_displayMsg info', false); 38 | } 39 | 40 | /** 41 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 42 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 43 | */ 44 | public function testDisplayMsg_ThrowExceptionWhenUnknownTypeWithMsg () 45 | { 46 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: ."); 47 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=(); CUI_displayMsg info blabla', false); 48 | } 49 | 50 | /** 51 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 52 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 53 | */ 54 | public function testDisplayMsg_Simple () 55 | { 56 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([info]=\'\033[0;36m\'); CUI_displayMsg info bla', false); 57 | $this->assertEquals('\033[0;36mbla\033[0m', $sMsg); 58 | } 59 | 60 | /** 61 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 62 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 63 | */ 64 | public function testDisplayMsg_SimpleWithMultipleMsg () 65 | { 66 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([info]=\'\033[0;36m\'); CUI_displayMsg info bla bla bla', false); 67 | $this->assertEquals('\033[0;36mbla bla bla\033[0m', $sMsg); 68 | } 69 | 70 | /** 71 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 72 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 73 | */ 74 | public function testDisplayMsg_ThrowExceptionWhenOnlyHeader () 75 | { 76 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: ."); 77 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([info.header]=\'\033[0;36m\'); CUI_displayMsg info bla', false); 78 | } 79 | 80 | /** 81 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 82 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 83 | */ 84 | public function testDisplayMsg_WithHeader () 85 | { 86 | $sMsg = $this->_localShellCodeCall( 87 | 'CUI_COLORS=([info]=\'\033[0;36m\' [info.header]=\'\033[1;36m(i) \'); ' 88 | . 'CUI_displayMsg info bla bla', false 89 | ); 90 | $this->assertEquals('\033[1;36m(i) \033[0;36mbla bla\033[0m', $sMsg); 91 | } 92 | 93 | /** 94 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 95 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 96 | */ 97 | public function testDisplayMsg_ThrowExceptionWhenOnlyBold () 98 | { 99 | $this->setExpectedException('RuntimeException', "Unknown display type 'info'!\nAvailable types: ."); 100 | $sMsg = $this->_localShellCodeCall('CUI_COLORS=([info.bold]=\'\033[0;36m\'); CUI_displayMsg info bla', false); 101 | } 102 | 103 | /** 104 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 105 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 106 | */ 107 | public function testDisplayMsg_WithBold () 108 | { 109 | $sMsg = $this->_localShellCodeCall( 110 | 'CUI_COLORS=([info]=\'\033[0;36m\' [info.bold]=\'\033[1;36m\'); ' 111 | . 'CUI_displayMsg info \"blahahabla\"', false 112 | ); 113 | $this->assertEquals('\033[0;36mbla\033[1;36mhaha\033[0;36mbla\033[0m', $sMsg); 114 | } 115 | 116 | /** 117 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 118 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 119 | */ 120 | public function testDisplayMsg_WithMultipleBoldTags () 121 | { 122 | $sMsg = $this->_localShellCodeCall( 123 | 'CUI_COLORS=([info]=\'\033[0;36m\' [info.bold]=\'\033[1;36m\'); ' 124 | . 'CUI_displayMsg info \"blahaha-Hello!bla\"', false 125 | ); 126 | $this->assertEquals('\033[0;36mbla\033[1;36mhaha\033[0;36m-\033[1;36mHello!\033[0;36mbla\033[1;36m\033[0;36m\033[0m', $sMsg); 127 | } 128 | 129 | /** 130 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 131 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 132 | */ 133 | public function testDisplayMsg_WithBoldTagsButWithoutBold () 134 | { 135 | $sMsg = $this->_localShellCodeCall( 136 | 'CUI_COLORS=([info]=\'\033[0;36m\'); ' 137 | . 'CUI_displayMsg info \"blahahabla\"', false 138 | ); 139 | $this->assertEquals('\033[0;36mblahahabla\033[0m', $sMsg); 140 | } 141 | 142 | /** 143 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 144 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 145 | */ 146 | public function testDisplayMsg_WithMultipleBoldTagsButWithoutBold () 147 | { 148 | $sMsg = $this->_localShellCodeCall( 149 | 'CUI_COLORS=([info]=\'\033[0;36m\'); ' 150 | . 'CUI_displayMsg info \"blahaha-Hello!bla\"', false 151 | ); 152 | $this->assertEquals('\033[0;36mblahaha-Hello!bla\033[0m', $sMsg); 153 | } 154 | 155 | /** 156 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 157 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 158 | */ 159 | public function testDisplayMsg_WithBoldAndHeader () 160 | { 161 | $sMsg = $this->_localShellCodeCall( 162 | 'CUI_COLORS=([info]=\'\033[0;36m\' [info.bold]=\'\033[1;36m\' [info.header]=\'\033[1;36m(i) \'); ' 163 | . 'CUI_displayMsg info \"blahahabla\"', false 164 | ); 165 | $this->assertEquals('\033[1;36m(i) \033[0;36mbla\033[1;36mhaha\033[0;36mbla\033[0m', $sMsg); 166 | } 167 | 168 | /** 169 | * @shcovers inc/coloredUI.inc.sh::CUI_isSet 170 | * @shcovers inc/coloredUI.inc.sh::CUI_displayMsg 171 | */ 172 | public function testDisplayMsg_WithBoldAndHeaderAndBackslashes () 173 | { 174 | $sMsg = $this->_localShellCodeCall( 175 | 'CUI_COLORS=([info]=\'\033[0;36m\' [info.bold]=\'/&\\\' [info.header]=\'/i\\ \'); ' 176 | . 'CUI_displayMsg info \"blahahabla\"', false 177 | ); 178 | $this->assertEquals('/i\ \033[0;36mbla/&\haha\033[0;36mbla\033[0m', $sMsg); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /twgit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # git clone git@github.com:Twenga/twgit.git [path] 7 | # cd [path] 8 | # Install: sudo make install --always-make 9 | # ou /bin/bash [path]/twgit 10 | # 11 | # Sur le modèle de http://github.com/nvie/gitflow 12 | 13 | # Dans ~/.bash_profile : 14 | # function get_git_branch { 15 | # local branch=$(git branch --no-color 2>/dev/null | grep -P '^\*' | sed 's/* //') 16 | # if [ ! -z "$branch" ]; then 17 | # echo -n " \[\e[1;30m\]git\[\e[1;35m\]$branch" 18 | # [ `git status --porcelain --ignore-submodules=all | wc -l` -ne 0 ] && echo '*' 19 | # fi 20 | # } 21 | # Puis : 22 | # export PROMPT_COMMAND='PS1="\[\e[0;32m\]\h:\w$(get_git_branch)\[\e[1;32m\]\\$\[\e[m\] "' 23 | # 24 | # 25 | # 26 | # Copyright (c) 2011 Twenga SA 27 | # Copyright (c) 2012-2013 Geoffroy Aubry 28 | # Copyright (c) 2013 Sebastien Hanicotte 29 | # 30 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 31 | # with the License. You may obtain a copy of the License at 32 | # 33 | # http://www.apache.org/licenses/LICENSE-2.0 34 | # 35 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 36 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 37 | # for the specific language governing permissions and limitations under the License. 38 | # 39 | # @copyright 2011 Twenga SA 40 | # @copyright 2012-2013 Geoffroy Aubry 41 | # @copyright 2013 Sebastien Hanicotte 42 | # @license http://www.apache.org/licenses/LICENSE-2.0 43 | # 44 | 45 | 46 | 47 | ## 48 | # Configuration 49 | # 50 | 51 | # Pre config: 52 | # Absolute path of the top-level directory of the current user repository: 53 | TWGIT_USER_REPOSITORY_ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" 54 | 55 | # Includes : 56 | path="$0"; while [ -h "$path" ]; do path="$(readlink "$path")"; done 57 | shared_config_file="$(dirname $path)/conf/twgit.sh" 58 | template_config_file="${shared_config_file%.sh}-dist.sh" 59 | local_config_file="${TWGIT_USER_REPOSITORY_ROOT_DIR:-.}/.twgit" 60 | if [ ! -f "$shared_config_file" ]; then 61 | echo "/!\\ Config file not found: '$shared_config_file'" 62 | echo "Try to copy '$template_config_file' to '$shared_config_file' and configure it." 63 | exit 1 64 | fi 65 | 66 | # Load default config: 67 | . $template_config_file 68 | 69 | # Load user config with color overloading management: 70 | declare -A colors_copy 71 | for key in "${!CUI_COLORS[@]}"; do colors_copy[$key]="${CUI_COLORS[$key]}"; done 72 | . $shared_config_file 73 | for key in "${!colors_copy[@]}"; do 74 | [ -z "${CUI_COLORS[$key]-}" ] && CUI_COLORS[$key]="${colors_copy[$key]}" 75 | done 76 | 77 | # Load repository config with color overloading management: 78 | if [ -f "$local_config_file" ]; then 79 | for key in "${!CUI_COLORS[@]}"; do colors_copy[$key]="${CUI_COLORS[$key]}"; done 80 | . $local_config_file 81 | for key in "${!colors_copy[@]}"; do 82 | [ -z "${CUI_COLORS[$key]-}" ] && CUI_COLORS[$key]="${colors_copy[$key]}" 83 | done 84 | fi 85 | 86 | . $TWGIT_INC_DIR/common.inc.sh 87 | 88 | # Post config: 89 | # TWGIT_USER_REPOSITORY_ROOT_DIR is absolute path of the top-level directory of the current user repository 90 | TWGIT_FEATURES_SUBJECT_PATH="$TWGIT_USER_REPOSITORY_ROOT_DIR/$TWGIT_FEATURES_SUBJECT_FILENAME" 91 | 92 | 93 | 94 | ## 95 | # Duplication des flux à des fins d'historisation 96 | # 97 | log_date="$(date "+%Y-%m-%d %H:%M:%S")" 98 | log_call="$0 $@" 99 | 100 | # Log errors: 101 | printf -- "$TWGIT_HISTORY_SEPARATOR" "$log_date" "$log_call" >> $TWGIT_HISTORY_ERROR_PATH 102 | PIPE_ERROR=$TWGIT_TMP_DIR/twgit_$$_error.tmp 103 | mkfifo "$PIPE_ERROR" 104 | tee -a <$PIPE_ERROR $TWGIT_HISTORY_ERROR_PATH & 105 | exec 2>&- 106 | exec 2>$PIPE_ERROR 107 | 108 | # Log display: 109 | printf -- "$TWGIT_HISTORY_SEPARATOR" "$log_date" "$log_call" >> $TWGIT_HISTORY_LOG_PATH 110 | PIPE_LOG=$TWGIT_TMP_DIR/twgit_$$_log.tmp 111 | mkfifo "$PIPE_LOG" 112 | tee -a <$PIPE_LOG $TWGIT_HISTORY_LOG_PATH & 113 | exec 1>&- 114 | exec 1>$PIPE_LOG 115 | 116 | # Clean on exit: 117 | trap "rm -f $PIPE_LOG $PIPE_ERROR" EXIT ERR 118 | 119 | 120 | 121 | ## 122 | # Affiche l'aide 123 | # 124 | # @tested_by TwgitHelpTest 125 | # 126 | function usage () { 127 | echo; CUI_displayMsg help 'Usage:' 128 | CUI_displayMsg help_detail 'twgit []' 129 | CUI_displayMsg help_detail ' Always provide branch names without any prefix:' 130 | CUI_displayMsg help_detail " – '$TWGIT_PREFIX_FEATURE', '$TWGIT_PREFIX_DEMO', '$TWGIT_PREFIX_RELEASE', '$TWGIT_PREFIX_HOTFIX', (tag) '$TWGIT_PREFIX_TAG'" 131 | 132 | echo; CUI_displayMsg help 'Available commands are:' 133 | CUI_displayMsg help_detail 'feature Manage your feature branches.' 134 | CUI_displayMsg help_detail 'demo Manage your demo branches.' 135 | CUI_displayMsg help_detail 'release Manage your release branches.' 136 | CUI_displayMsg help_detail 'hotfix Manage your hotfix branches.' 137 | CUI_displayMsg help_detail 'tag Manage your tags.' 138 | echo 139 | CUI_displayMsg help_detail 'clean Help to remove branches no longer tracked.' 140 | CUI_displayMsg help_detail 'init []' 141 | CUI_displayMsg help_detail ' Initialize git repository for twgit:' 142 | CUI_displayMsg help_detail ' – git init if necessary' 143 | CUI_displayMsg help_detail " – add remote $TWGIT_ORIGIN if necessary" 144 | CUI_displayMsg help_detail " – create '$TWGIT_STABLE' branch if not exists, or pull '$TWGIT_ORIGIN/$TWGIT_STABLE'" 145 | CUI_displayMsg help_detail " branch if exists" 146 | CUI_displayMsg help_detail ' – create tag on HEAD of stable, e.g. 1.2.3, using' 147 | CUI_displayMsg help_detail ' major.minor.revision format. ' 148 | CUI_displayMsg help_detail " Prefix '$TWGIT_PREFIX_TAG' will be added to the specified ." 149 | CUI_displayMsg help_detail ' A remote repository must exists.' 150 | CUI_displayMsg help_detail 'update Force update twgit check.' 151 | CUI_displayMsg help_detail '[help] Display this help.' 152 | echo; CUI_displayMsg help 'See also:' 153 | CUI_displayMsg help_detail "Try 'twgit [help]' for more details." 154 | 155 | echo; CUI_displayMsg help 'About:' 156 | CUI_displayMsg help_detail "Contact https://github.com/Twenga/twgit" 157 | CUI_displayMsg help_detail "Git repository git@github.com:Twenga/twgit.git" 158 | local version=$(cd $TWGIT_ROOT_DIR && git describe) 159 | CUI_displayMsg help_detail "Version $version" 160 | local last_update_timestamp=$(getLastUpdateTimestamp "$TWGIT_UPDATE_PATH") 161 | local last_update_date="$(getDateFromTimestamp "$last_update_timestamp")" 162 | local next_update_timestamp=$(( $last_update_timestamp + $TWGIT_UPDATE_NB_DAYS * 86400 )) 163 | local next_update_date="$(getDateFromTimestamp "$next_update_timestamp")" 164 | CUI_displayMsg help_detail "Last check for update $last_update_date" 165 | CUI_displayMsg help_detail "Next check for update $next_update_date" 166 | echo 167 | } 168 | 169 | ## 170 | # Action déclenchant l'affichage de l'aide. 171 | # 172 | # @tested_by TwgitHelpTest 173 | # 174 | function cmd_help () { 175 | usage; 176 | } 177 | 178 | ## 179 | # Appelle l'action demandée ("cmd_$2") sur la commande spécifiée ($1). 180 | # Les paramètres surnumérères sont passés à la commande. 181 | # 182 | # @param string $1 commande demandée 183 | # @param string $2 nom de l'action demandée, 'help' par défaut 184 | # 185 | function main () { 186 | # load command: 187 | if [ ! -z "$1" ]; then 188 | local command="$1"; shift 189 | guess_dyslexia "$command" && command="$RETVAL" 190 | local command_file="$TWGIT_INC_DIR/twgit_$command.inc.sh"; 191 | if [ "$command" = 'help' ]; then 192 | usage; exit 0 193 | elif [ "$command" = 'update' ]; then 194 | autoupdate 'force'; exit 0 195 | elif [ "$command" = 'clean' ]; then 196 | clean_branches; exit 0 197 | elif [ "$command" = 'init' ]; then 198 | init "$@"; exit 0 199 | elif [ ! -e "$command_file" ]; then 200 | CUI_displayMsg error "Command not found: '$command'" 201 | usage 202 | exit 1 203 | else 204 | . "$command_file" 205 | fi 206 | fi 207 | 208 | # run the specified action: 209 | local action='help' 210 | if [ ! -z "$1" ]; then 211 | action="$1"; shift 212 | fi 213 | guess_dyslexia "$action" && action="$RETVAL" 214 | local action_func_name="cmd_$action" 215 | if ! type "$action_func_name" >/dev/null 2>&1; then 216 | CUI_displayMsg error "Unknown action: '$action'" 217 | usage 218 | exit 1 219 | else 220 | [ "$action" != 'help' ] && assert_git_repository 221 | $action_func_name "$@" 222 | fi 223 | } 224 | 225 | [ "$1" != "update" ] && [ "$TWGIT_UPDATE_AUTO" = "1" ] && autoupdate 'no-force' $* 226 | assert_git_configured 227 | assert_connectors_well_configured 228 | main "$@" 229 | -------------------------------------------------------------------------------- /inc/twgit_hotfix.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2011 Twenga SA 9 | # Copyright (c) 2012-2013 Geoffroy Aubry 10 | # Copyright (c) 2013 Cyrille Hemidy 11 | # Copyright (c) 2013 Sebastien Hanicotte 12 | # Copyright (c) 2014 Laurent Toussaint 13 | # 14 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 15 | # with the License. You may obtain a copy of the License at 16 | # 17 | # http://www.apache.org/licenses/LICENSE-2.0 18 | # 19 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 20 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 21 | # for the specific language governing permissions and limitations under the License. 22 | # 23 | # @copyright 2011 Twenga SA 24 | # @copyright 2012-2013 Geoffroy Aubry 25 | # @copyright 2013 Cyrille Hemidy 26 | # @copyright 2013 Sebastien Hanicotte 27 | # @copyright 2014 Laurent Toussaint 28 | # @license http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | 31 | 32 | 33 | ## 34 | # Affiche l'aide de la commande tag. 35 | # 36 | # @testedby TwgitHelpTest 37 | # 38 | function usage () { 39 | echo; CUI_displayMsg help 'Usage:' 40 | CUI_displayMsg help_detail 'twgit hotfix ' 41 | echo; CUI_displayMsg help 'Available actions are:' 42 | CUI_displayMsg help_detail 'finish [-I]' 43 | CUI_displayMsg help_detail " Merge current hotfix branch into '$TWGIT_STABLE', create a new tag and push." 44 | CUI_displayMsg help_detail ' Add -I to run in non-interactive mode (always say yes).'; echo 45 | CUI_displayMsg help_detail 'list [-F]' 46 | CUI_displayMsg help_detail ' List current hotfix. Add -F to do not make fetch.'; echo 47 | CUI_displayMsg help_detail 'push' 48 | CUI_displayMsg help_detail " Push current hotfix to '$TWGIT_ORIGIN' repository." 49 | CUI_displayMsg help_detail " It's a shortcut for: \"git push $TWGIT_ORIGIN ${TWGIT_PREFIX_HOTFIX}…\""; echo 50 | CUI_displayMsg help_detail 'remove ' 51 | CUI_displayMsg help_detail ' Remove both local and remote specified hotfix branch.' 52 | CUI_displayMsg help_detail ' Despite that, create the same tag as finish action to clearly distinguish' 53 | CUI_displayMsg help_detail ' the next hotfix from this one.' 54 | CUI_displayMsg help_detail " Prefix '$TWGIT_PREFIX_HOTFIX' will be added to the specified ."; echo 55 | CUI_displayMsg help_detail 'start [-I]' 56 | CUI_displayMsg help_detail ' Create both a new local and remote hotfix, or fetch the remote hotfix,' 57 | CUI_displayMsg help_detail ' or checkout the local hotfix.' 58 | CUI_displayMsg help_detail ' Hotfix name will be generated by incrementing revision of the last tag:' 59 | CUI_displayMsg help_detail " v1.2.3 > ${TWGIT_PREFIX_HOTFIX}1.2.4"; 60 | CUI_displayMsg help_detail ' Add -I to run in non-interactive mode (always say yes).'; echo 61 | CUI_displayMsg help_detail '[help]' 62 | CUI_displayMsg help_detail ' Display this help.'; echo 63 | } 64 | 65 | ## 66 | # Action déclenchant l'affichage de l'aide. 67 | # 68 | # @testedby TwgitHelpTest 69 | # 70 | function cmd_help () { 71 | usage; 72 | } 73 | 74 | ## 75 | # Liste les derniers hotfixes. 76 | # Gère l'option '-F' permettant d'éviter le fetch. 77 | # 78 | function cmd_list () { 79 | process_options "$@" 80 | process_fetch 'F' 81 | 82 | local hotfixes=$(git branch --no-color -r --merged $TWGIT_ORIGIN/$TWGIT_STABLE | grep "$TWGIT_ORIGIN/$TWGIT_PREFIX_HOTFIX" | sed 's/^[* ]*//') 83 | if [ ! -z "$hotfixes" ]; then 84 | CUI_displayMsg help "Remote hotfixes merged into '$TWGIT_STABLE':" 85 | CUI_displayMsg warning "A hotfixes must be deleted after merge into '$TWGIT_STABLE'! Following hotfixes should not exists!" 86 | display_branches 'hotfix' "$hotfixes" 87 | echo 88 | fi 89 | 90 | local hotfix=$(get_current_hotfix_in_progress) 91 | if [ ! -z "$hotfix" ]; then 92 | CUI_displayMsg help "Remote current hotfix:" 93 | display_super_branch 'hotfix' "$hotfix" 94 | else 95 | display_branches 'hotfix' '' 96 | fi 97 | echo 98 | 99 | alert_dissident_branches 100 | } 101 | 102 | ## 103 | # Crée un nouveau hotfix à partir du dernier tag. 104 | # Son nom est le dernier tag en incrémentant le numéro de révision : major.minor.(revision+1) 105 | # 106 | function cmd_start () { 107 | process_options "$@" 108 | require_parameter '-' 109 | assert_clean_working_tree 110 | process_fetch 111 | 112 | CUI_displayMsg processing 'Check remote hotfixes...' 113 | local remote_hotfix="$(get_hotfixes_in_progress)" 114 | local hotfix 115 | if [ -z "$remote_hotfix" ]; then 116 | assert_tag_exists 117 | local last_tag=$(get_last_tag) 118 | hotfix=$(get_next_version 'revision') 119 | local hotfix_fullname="$TWGIT_PREFIX_HOTFIX$hotfix" 120 | exec_git_command "git checkout -b $hotfix_fullname tags/$last_tag" "Could not check out tag '$last_tag'!" 121 | 122 | update_version_information "$hotfix" 123 | 124 | process_first_commit 'hotfix' "$hotfix_fullname" 125 | process_push_branch $hotfix_fullname 126 | else 127 | local prefix="$TWGIT_ORIGIN/$TWGIT_PREFIX_HOTFIX" 128 | hotfix="${remote_hotfix:${#prefix}}" 129 | CUI_displayMsg processing "Remote hotfix '$TWGIT_PREFIX_HOTFIX$hotfix' detected." 130 | assert_valid_ref_name $hotfix 131 | 132 | is_initial_author $hotfix 'hotfix' 133 | 134 | local hotfix_fullname="$TWGIT_PREFIX_HOTFIX$hotfix" 135 | assert_new_local_branch $hotfix_fullname 136 | exec_git_command "git checkout --track -b $hotfix_fullname $remote_hotfix" "Could not check out hotfix '$remote_hotfix'!" 137 | fi 138 | echo 139 | } 140 | 141 | ## 142 | # Supprime le hotfix spécifié. 143 | # 144 | # @param string $1 nom court du hotfix 145 | # 146 | function cmd_remove () { 147 | process_options "$@" 148 | require_parameter 'hotfix' 149 | clean_prefixes "$RETVAL" 'hotfix' 150 | local hotfix="$RETVAL" 151 | local hotfix_fullname="$TWGIT_PREFIX_HOTFIX$hotfix" 152 | local tag="$hotfix" 153 | local tag_fullname="$TWGIT_PREFIX_TAG$tag" 154 | 155 | assert_valid_ref_name $hotfix 156 | assert_clean_working_tree 157 | assert_working_tree_is_not_on_delete_branch $hotfix_fullname 158 | 159 | process_fetch 160 | assert_new_and_valid_tag_name $tag 161 | 162 | # Suppression de la branche : 163 | assert_clean_stable_branch_and_checkout 164 | remove_local_branch $hotfix_fullname 165 | remove_remote_branch $hotfix_fullname 166 | 167 | # Gestion du tag : 168 | create_and_push_tag "$tag_fullname" "Hotfix remove: $hotfix_fullname" 169 | echo 170 | } 171 | 172 | ## 173 | # Merge le hotfix à la branche stable et crée un tag portant son nom. 174 | # Gère l'option '-I' permettant de répondre automatiquement (mode non interactif) oui à la demande de pull. 175 | # 176 | # @param string $1 nom court du hotfix 177 | # 178 | function cmd_finish () { 179 | process_options "$@" 180 | assert_clean_working_tree 181 | process_fetch 182 | 183 | CUI_displayMsg processing 'Check remote hotfix...' 184 | local remote_hotfix="$(get_hotfixes_in_progress)" 185 | [ -z "$remote_hotfix" ] && die 'No hotfix in progress!' 186 | 187 | local prefix="$TWGIT_ORIGIN/$TWGIT_PREFIX_HOTFIX" 188 | hotfix="${remote_hotfix:${#prefix}}" 189 | local hotfix_fullname="$TWGIT_PREFIX_HOTFIX$hotfix" 190 | CUI_displayMsg processing "Remote hotfix '$hotfix_fullname' detected." 191 | 192 | CUI_displayMsg processing "Check local branch '$hotfix_fullname'..." 193 | if has $hotfix_fullname $(get_local_branches); then 194 | assert_branches_equal "$hotfix_fullname" "$TWGIT_ORIGIN/$hotfix_fullname" 195 | else 196 | exec_git_command "git checkout --track -b $hotfix_fullname $TWGIT_ORIGIN/$hotfix_fullname" "Could not check out hotfix '$TWGIT_ORIGIN/$hotfix_fullname'!" 197 | fi 198 | 199 | # Gestion du tag : 200 | local tag="$hotfix" 201 | local tag_fullname="$TWGIT_PREFIX_TAG$tag" 202 | assert_new_and_valid_tag_name $tag 203 | 204 | assert_clean_stable_branch_and_checkout 205 | exec_git_command "git merge --no-ff $hotfix_fullname" "Could not merge '$hotfix_fullname' into '$TWGIT_STABLE'!" 206 | create_and_push_tag "$tag_fullname" "Hotfix finish: $hotfix_fullname" 207 | 208 | # Suppression de la branche : 209 | remove_local_branch $hotfix_fullname 210 | remove_remote_branch $hotfix_fullname 211 | 212 | local current_release="$(get_current_release_in_progress)" 213 | [ ! -z "$current_release" ] \ 214 | && CUI_displayMsg warning "Do not forget to merge '$tag_fullname' tag into '$TWGIT_ORIGIN/$current_release' release before close it! Try on release: git merge --no-ff $tag_fullname" 215 | echo 216 | } 217 | 218 | ## 219 | # Push du hotfix. 220 | # 221 | function cmd_push () { 222 | local current_branch=$(get_current_branch) 223 | local remote_hotfix="$(get_hotfixes_in_progress)" 224 | if [ "$TWGIT_ORIGIN/$current_branch" != "$remote_hotfix" ]; then 225 | die "You must be in a hotfix to launch this command!" 226 | fi 227 | process_push_branch "$current_branch" 228 | } 229 | -------------------------------------------------------------------------------- /tests/lib/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | 19 | * @license http://www.apache.org/licenses/LICENSE-2.0 20 | */ 21 | class ErrorHandler 22 | { 23 | 24 | /** 25 | * Traduction des codes d'erreurs PHP. 26 | * @var array 27 | * @see internalErrorHandler() 28 | */ 29 | public static $aErrorTypes = array( 30 | E_ERROR => 'ERROR', 31 | E_WARNING => 'WARNING', 32 | E_PARSE => 'PARSING ERROR', 33 | E_NOTICE => 'NOTICE', 34 | E_CORE_ERROR => 'CORE ERROR', 35 | E_CORE_WARNING => 'CORE WARNING', 36 | E_COMPILE_ERROR => 'COMPILE ERROR', 37 | E_COMPILE_WARNING => 'COMPILE WARNING', 38 | E_USER_ERROR => 'USER ERROR', 39 | E_USER_WARNING => 'USER WARNING', 40 | E_USER_NOTICE => 'USER NOTICE', 41 | E_STRICT => 'STRICT NOTICE', 42 | E_RECOVERABLE_ERROR => 'RECOVERABLE ERROR' 43 | ); 44 | 45 | /** 46 | * Code d'erreur accompagnant les exceptions générées par internalErrorHandler() et log(). 47 | * @var int 48 | * @see internalErrorHadler() 49 | * @see log() 50 | */ 51 | private static $_iDefaultErrorCode = 1; 52 | 53 | /** 54 | * Doit-on afficher les erreurs (à l'écran ou dans le canal d'erreur en mode CLI). 55 | * @var bool 56 | */ 57 | private $_bDisplayErrors; 58 | 59 | /** 60 | * Chemin du fichier de log d'erreur. 61 | * @var string 62 | */ 63 | private $_sErrorLogPath; 64 | 65 | /** 66 | * Seuil d'erreur. 67 | * @var int 68 | */ 69 | private $_iErrorReporting; 70 | 71 | /** 72 | * Autorise l'usage de l'opérateur de suppression d'erreur ou non ('@'). 73 | * @var bool 74 | */ 75 | private $_bAuthErrSupprOp; 76 | 77 | /** 78 | * Est-on en mode CLI. 79 | * @var bool 80 | */ 81 | private $_bIsRunningFromCLI; 82 | 83 | /** 84 | * Recense les répertoires exclus du spectre du gestionnaire interne d'erreur. 85 | * 86 | * @var array 87 | * @see addExcludedPath() 88 | */ 89 | private $_aExcludedPaths; 90 | 91 | private $_sCallbackGenericDisplay; 92 | 93 | /** 94 | * Constructeur. 95 | * 96 | * @param bool $bDisplayErrors affiche ou non les erreurs à l'écran ou dans le canal d'erreur en mode CLI 97 | * @param string $sErrorLogPath chemin du fichier de log d'erreur 98 | * @param int $iErrorReporting Seuil de remontée d'erreur, transmis à error_reporting() 99 | * @param bool $bAuthErrSupprOp autoriser ou non l'usage de l'opérateur de suppression d'erreur ('@') 100 | */ 101 | public function __construct ($bDisplayErrors=true, $sErrorLogPath='', $iErrorReporting=-1, $bAuthErrSupprOp=false) 102 | { 103 | $this->_bDisplayErrors = $bDisplayErrors; 104 | $this->_sErrorLogPath = $sErrorLogPath; 105 | $this->_iErrorReporting = $iErrorReporting; 106 | $this->_bAuthErrSupprOp = $bAuthErrSupprOp; 107 | $this->_aExcludedPaths = array(); 108 | $this->_bIsRunningFromCLI = defined('STDIN'); // ou (PHP_SAPI === 'cli') 109 | $this->_sCallbackGenericDisplay = array($this, 'displayDefaultApologies'); 110 | 111 | error_reporting($iErrorReporting); 112 | ini_set('display_errors', $bDisplayErrors); 113 | ini_set('log_errors', true); 114 | ini_set('html_errors', false); 115 | ini_set('display_startup_errors', true); 116 | if ($sErrorLogPath !== '') { 117 | ini_set('error_log', $sErrorLogPath); 118 | } 119 | ini_set('ignore_repeated_errors', true); 120 | ini_set('max_execution_time', 0); 121 | 122 | // Make sure we have a timezone for date functions. It is not safe to rely on the system's timezone settings. 123 | // Please use the date.timezone setting, the TZ environment variable 124 | // or the date_default_timezone_set() function. 125 | if (ini_get('date.timezone') == '') { 126 | date_default_timezone_set('Europe/Paris'); 127 | } 128 | 129 | set_error_handler(array($this, 'internalErrorHandler')); 130 | set_exception_handler(array($this, 'internalExceptionHandler')); 131 | register_shutdown_function(array($this, 'internalShutdownFunction')); 132 | } 133 | 134 | /** 135 | * Exclu un répertoire du spectre du gestionnaire interne d'erreur. 136 | * Utile par exemple pour exclure une librairie codée en PHP4 et donc dépréciée. 137 | * Le '/' en fin de chaîne n'est pas obligatoire. 138 | * 139 | * @param string $sPath 140 | * @see internalErrorHandler() 141 | */ 142 | public function addExcludedPath ($sPath) 143 | { 144 | if (substr($sPath, -1) !== '/') { 145 | $sPath .= '/'; 146 | } 147 | $sPath = realpath($sPath); 148 | if ( ! in_array($sPath, $this->_aExcludedPaths)) { 149 | $this->_aExcludedPaths[] = $sPath; 150 | } 151 | } 152 | 153 | public function setCallbackGenericDisplay ($sCallbackGenericDisplay) 154 | { 155 | $this->_sCallbackGenericDisplay = $sCallbackGenericDisplay; 156 | } 157 | 158 | /** 159 | * Customized error handler function: throws an Exception with the message error if @ operator not used 160 | * and error source is not in excluded paths. 161 | * 162 | * @param int $iErrNo level of the error raised. 163 | * @param string $sErrStr the error message. 164 | * @param string $sErrFile the filename that the error was raised in. 165 | * @param int $iErrLine the line number the error was raised at. 166 | * @return boolean true, then the normal error handler does not continues. 167 | * @see addExcludedPath() 168 | */ 169 | public function internalErrorHandler ($iErrNo, $sErrStr, $sErrFile, $iErrLine) 170 | { 171 | // Si l'erreur provient d'un répertoire exclu de ce handler, alors l'ignorer. 172 | foreach ($this->_aExcludedPaths as $sExcludedPath) { 173 | if (stripos($sErrFile, $sExcludedPath) === 0) { 174 | return true; 175 | } 176 | } 177 | 178 | // Gestion de l'éventuel @ (error suppression operator) : 179 | if ($this->_iErrorReporting !== 0 && error_reporting() === 0 && $this->_bAuthErrSupprOp) { 180 | $iErrorReporting = 0; 181 | } else { 182 | $iErrorReporting = $this->_iErrorReporting; 183 | } 184 | 185 | // Le seuil de transformation en exception est-il atteint ? 186 | if (($iErrorReporting & $iErrNo) !== 0) { 187 | $msg = "[from error handler] " . self::$aErrorTypes[$iErrNo] 188 | . " -- $sErrStr, in file: '$sErrFile', line $iErrLine"; 189 | throw new ErrorException($msg, self::$_iDefaultErrorCode, $iErrNo, $sErrFile, $iErrLine); 190 | } 191 | return true; 192 | } 193 | 194 | /** 195 | * Gestionnaire d'exception. 196 | * Log systématiquement l'erreur. 197 | * 198 | * @param Exception $oException 199 | * @see log() 200 | */ 201 | public function internalExceptionHandler (Exception $oException) 202 | { 203 | if ( ! $this->_bDisplayErrors && ini_get('error_log') !== '' && ! $this->_bIsRunningFromCLI) { 204 | call_user_func($this->_sCallbackGenericDisplay, $oException); 205 | } 206 | $this->log($oException); 207 | } 208 | 209 | /** 210 | * Comportement ou message d'excuse sur erreur/exception non traitée lorsque l'affichage 211 | * des erreurs à l'écran est désactivé. 212 | */ 213 | public function displayDefaultApologies () 214 | { 215 | echo '
Une erreur d\'exécution est apparue.
' 216 | . 'Nous sommes désolés pour la gêne occasionée.
'; 217 | } 218 | 219 | public function internalShutdownFunction () 220 | { 221 | $aError = error_get_last(); 222 | if ($aError !== null && $aError['type'] === E_ERROR) { 223 | $oException = new ErrorException( 224 | $aError['message'], self::$_iDefaultErrorCode, $aError['type'], $aError['file'], $aError['line'] 225 | ); 226 | call_user_func($this->_sCallbackGenericDisplay, $oException); 227 | } 228 | } 229 | 230 | /** 231 | * Log l'erreur spécifiée dans le fichier de log si défini. 232 | * Si l'affichage des erreurs est activé, alors envoi l'erreur sur le canal d'erreur en mode CLI, 233 | * ou réalise un print_r sinon. 234 | * 235 | * @param mixed $mError Erreur à loguer, tableau ou objet. 236 | */ 237 | public function log ($mError) 238 | { 239 | if ($this->_bDisplayErrors) { 240 | if ($this->_bIsRunningFromCLI) { 241 | file_put_contents('php://stderr', $mError . "\n", E_USER_ERROR); 242 | $iErrorCode = ($mError instanceof Exception ? $mError->getCode() : self::$_iDefaultErrorCode); 243 | exit($iErrorCode); 244 | } else { 245 | print_r($mError); 246 | } 247 | } 248 | 249 | if ( ! empty($this->_sErrorLogPath)) { 250 | if (is_array($mError) || (is_object($mError) && ! ($mError instanceof Exception))) { 251 | $mError = print_r($mError, true); 252 | } 253 | error_log($mError); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /tests/inc/TwgitTestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Geoffroy Letournel 9 | */ 10 | class TwgitTestCase extends PHPUnit_Framework_TestCase 11 | { 12 | /** 13 | * The name of the Git "stable" branch 14 | */ 15 | const STABLE = TWGIT_STABLE; 16 | 17 | /** 18 | * The shortname of the Git remote 19 | */ 20 | const ORIGIN = TWGIT_ORIGIN; 21 | 22 | /** 23 | * @var string The name of the remote "stable" branch 24 | */ 25 | protected static $_remoteStable; 26 | 27 | /** 28 | * Répertoire des dépôt locaux. 29 | * @var array 30 | */ 31 | private static $_aLocalRepositoriesDir = array( 32 | 1 => TWGIT_REPOSITORY_LOCAL_DIR, 33 | 2 => TWGIT_REPOSITORY_SECOND_LOCAL_DIR 34 | ); 35 | 36 | /** 37 | * @var Shell_Adapter 38 | */ 39 | protected static $_oShell = NULL; 40 | 41 | /** 42 | * @var string 43 | * @see setUp(); 44 | */ 45 | private static $_sSetUpCmd = ''; 46 | 47 | /** 48 | * Singleton. 49 | * 50 | * @return Shell_Adapter 51 | */ 52 | protected static function _getShellInstance () 53 | { 54 | if (self::$_oShell === NULL) { 55 | self::$_oShell = new Shell_Adapter(); 56 | } 57 | return self::$_oShell; 58 | } 59 | 60 | /** 61 | * Exécute la commande shell spécifiée et retourne la sortie découpée par ligne dans un tableau. 62 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 63 | * 64 | * @param string $sCmd 65 | * @return array tableau indexé du flux de sortie shell découpé par ligne 66 | * @throws RuntimeException en cas d'erreur shell 67 | */ 68 | protected static function _rawExec ($sCmd) 69 | { 70 | return self::_getShellInstance()->exec($sCmd); 71 | } 72 | 73 | /** 74 | * Constructs a test case with the given name. 75 | * 76 | * @param string $name 77 | * @param array $data 78 | * @param string $dataName 79 | */ 80 | public function __construct($sName=NULL, array $aData=array(), $sDataName='') 81 | { 82 | parent::__construct($sName, $aData, $sDataName); 83 | self::$_remoteStable = self::ORIGIN . '/' . self::STABLE; 84 | } 85 | 86 | /** 87 | * Sets up the fixture, for example, open a network connection. 88 | * This method is called before a test is executed. 89 | */ 90 | public function setUp () 91 | { 92 | if (empty(self::$_sSetUpCmd)) { 93 | $aDir = array( 94 | TWGIT_REPOSITORY_ORIGIN_DIR, 95 | TWGIT_REPOSITORY_LOCAL_DIR, 96 | TWGIT_REPOSITORY_SECOND_LOCAL_DIR, 97 | TWGIT_REPOSITORY_SECOND_REMOTE_DIR, 98 | TWGIT_REPOSITORY_THIRD_REMOTE_DIR 99 | ); 100 | $aCmd = array(); 101 | foreach ($aDir as $sDir) { 102 | if (strpos($sDir, TWGIT_TMP_DIR . '/') !== 0) { 103 | throw new RuntimeException("Security check before 'rm -rf'…"); 104 | } 105 | $aCmd[] = "rm -rf '$sDir' && mkdir -p '$sDir' && chmod 0777 '$sDir'"; 106 | } 107 | self::$_sSetUpCmd = implode(' && ', $aCmd); 108 | } 109 | 110 | $this->_rawExec(self::$_sSetUpCmd); 111 | copy(TWGIT_TMP_DIR . '/conf-twgit.sh', TWGIT_REPOSITORY_LOCAL_DIR . '/.twgit'); 112 | copy(TWGIT_TMP_DIR . '/conf-twgit.sh', TWGIT_REPOSITORY_SECOND_LOCAL_DIR . '/.twgit'); 113 | } 114 | 115 | /** 116 | * Get the name of a remote branch. 117 | * 118 | * @param string $name The branch name (e.g. master, stable, hotfix-42) 119 | * @param string $remote The name of the Remote (e.g. origin) 120 | * 121 | * @return string Returns the name of the remote branch 122 | */ 123 | protected static function _remote($name, $remote = null) 124 | { 125 | if ($remote === null) { 126 | $remote = TWGIT_ORIGIN; 127 | } 128 | 129 | return $remote . '/' . $name; 130 | } 131 | 132 | /** 133 | * Get a list of remote branches. 134 | * 135 | * @param array $branches The branches names (e.g. master, issues, feature-1) 136 | * @param string $remote The name of the Remote (e.g. origin) 137 | * 138 | * @return array Returns a list of remote branches 139 | */ 140 | protected static function _remotes(array $branches, $remote = null) 141 | { 142 | if ($remote === null) { 143 | $remote = TWGIT_ORIGIN; 144 | } 145 | 146 | $result = array(); 147 | 148 | foreach ($branches as $branch) { 149 | $result[] = 'remotes/' . $remote . '/' . $branch; 150 | } 151 | 152 | return $result; 153 | } 154 | 155 | /** 156 | * Supprime les couleurs Shell du message spécifié. 157 | * 158 | * @param string $sMsg 159 | * @return string 160 | */ 161 | protected static function stripColors ($sMsg) 162 | { 163 | return preg_replace('/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/', '', $sMsg); 164 | } 165 | 166 | /** 167 | * Exécute la commande shell spécifiée et retourne la sortie d'exécution sous forme d'une chaîne de caractères. 168 | * L'éventuelle coloration Shell est enlevée. 169 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 170 | * 171 | * @param string $sCmd 172 | * @param bool $bStripBashColors Supprime ou non la coloration Bash de la chaîne retournée 173 | * @return string sortie d'exécution sous forme d'une chaîne de caractères. 174 | * @throws RuntimeException en cas d'erreur shell 175 | */ 176 | protected function _exec ($sCmd, $bStripBashColors=true) 177 | { 178 | try { 179 | $aResult = self::_rawExec($sCmd); 180 | } catch (RuntimeException $oException) { 181 | $sMsg = ($oException->getMessage() != '' ? $oException->getMessage() : '-- no message --'); 182 | throw new RuntimeException( 183 | self::stripColors($sMsg), 184 | $oException->getCode(), 185 | $oException 186 | ); 187 | } 188 | $sMsg = implode("\n", $aResult); 189 | if ($bStripBashColors) { 190 | $sMsg = preg_replace('/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/', '', $sMsg); 191 | } else { 192 | $sMsg = str_replace("\033", '\033', $sMsg); 193 | } 194 | return $sMsg; 195 | } 196 | 197 | /** 198 | * Exécute la commande shell spécifiée dans le répertoire du dépôt Git local, 199 | * et retourne la sortie d'exécution sous forme d'une chaîne de caractères. 200 | * L'éventuelle coloration Shell est enlevée. 201 | * 202 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 203 | * 204 | * @param string $sCmd 205 | * @param bool $bStripBashColors Supprime ou non la coloration Bash de la chaîne retournée 206 | * @param int $iWhichLocalDir Spécifie le dépôt local concerné 207 | * @return string sortie d'exécution sous forme d'une chaîne de caractères. 208 | * @throws RuntimeException en cas d'erreur shell 209 | * @see TwgitTestCase::$_aLocalRepositoriesDir 210 | */ 211 | protected function _localExec ($sCmd, $bStripBashColors=true, $iWhichLocalDir=1) 212 | { 213 | $sLocalCmd = 'cd ' . self::$_aLocalRepositoriesDir[$iWhichLocalDir] . ' && ' . $sCmd; 214 | return $this->_exec($sLocalCmd, $bStripBashColors); 215 | } 216 | 217 | /** 218 | * Appelle une fonction de inc/common.inc.sh une fois dans le répertoire du dépôt Git local, 219 | * et retourne la sortie d'exécution sous forme d'une chaîne de caractères. 220 | * L'éventuelle coloration Shell est enlevée. 221 | * Les fichiers de configuration Shell sont préalablement chargés. 222 | * 223 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 224 | * 225 | * Par exemple : $this->_localFunctionCall('process_fetch x'); 226 | * 227 | * @param string $sCmd 228 | * @param bool $bStripBashColors Supprime ou non la coloration Bash de la chaîne retournée 229 | * @param int $iWhichLocalDir Spécifie le dépôt local concerné 230 | * @return string sortie d'exécution sous forme d'une chaîne de caractères. 231 | * @throws RuntimeException en cas d'erreur shell 232 | * @see TwgitTestCase::$_aLocalRepositoriesDir 233 | */ 234 | protected function _localFunctionCall ($sCmd, $bStripBashColors=true, $iWhichLocalDir=1) 235 | { 236 | $sFunctionCall = TWGIT_BASH_EXEC . ' ' . TWGIT_TESTS_INC_DIR . '/testFunction.sh ' . $sCmd; 237 | return $this->_localExec($sFunctionCall, $bStripBashColors, $iWhichLocalDir); 238 | } 239 | 240 | /** 241 | * Exécute du code appelant des fonctions de inc/common.inc.sh une fois dans le répertoire du dépôt Git local, 242 | * et retourne la sortie d'exécution sous forme d'une chaîne de caractères. 243 | * L'éventuelle coloration Shell est enlevée. 244 | * Les fichiers de configuration Shell sont préalablement chargés. 245 | * 246 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 247 | * 248 | * Par exemple : $this->_localShellCodeCall('process_options x -aV; isset_option a; echo \$?'); 249 | * Attention à l'échappement des dollars ($). 250 | * 251 | * @param string $sCmd 252 | * @param bool $bStripBashColors Supprime ou non la coloration Bash de la chaîne retournée 253 | * @param int $iWhichLocalDir Spécifie le dépôt local concerné 254 | * @return string sortie d'exécution sous forme d'une chaîne de caractères. 255 | * @throws RuntimeException en cas d'erreur shell 256 | * @see TwgitTestCase::$_aLocalRepositoriesDir 257 | */ 258 | protected function _localShellCodeCall ($sCmd, $bStripBashColors=true, $iWhichLocalDir=1) 259 | { 260 | $sShellCodeCall = TWGIT_BASH_EXEC . ' ' . TWGIT_TESTS_INC_DIR . '/testShellCode.sh "' . $sCmd . '"'; 261 | return $this->_localExec($sShellCodeCall, $bStripBashColors, $iWhichLocalDir); 262 | } 263 | 264 | /** 265 | * Exécute la commande shell spécifiée dans le répertoire du dépôt Git distant, 266 | * et retourne la sortie d'exécution sous forme d'une chaîne de caractères. 267 | * L'éventuelle coloration Shell est enlevée. 268 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 269 | * 270 | * @param string $sCmd 271 | * @param bool $bStripBashColors Supprime ou non la coloration Bash de la chaîne retournée 272 | * @return string sortie d'exécution sous forme d'une chaîne de caractères. 273 | * @throws RuntimeException en cas d'erreur shell 274 | */ 275 | protected function _remoteExec ($sCmd, $bStripBashColors=true) 276 | { 277 | $sRemoteCmd = 'cd ' . TWGIT_REPOSITORY_ORIGIN_DIR . ' && ' . $sCmd; 278 | return $this->_exec($sRemoteCmd, $bStripBashColors); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /tests/lib/Shell/Interface.php: -------------------------------------------------------------------------------- 1 | 8 | * @license http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | interface Shell_Interface 11 | { 12 | 13 | /** 14 | * Exécute dans des processus parallèles les déclinaisons du pattern spécifié en fonction des valeurs. 15 | * Plusieurs lots de processus parallèles peuvent être générés si le nombre de valeurs 16 | * dépasse la limite $iMax. 17 | * 18 | * Exemple : $this->parallelize(array('aai@aai-01', 'prod@aai-01'), "ssh [] /bin/bash <parallelize(array('a', 'b'), 'cat /.../resources/[].txt'); 20 | * 21 | * @param array $aValues liste de valeurs qui viendront remplacer le(s) '[]' du pattern 22 | * @param string $sPattern pattern possédant une ou plusieurs occurences de paires de crochets vides '[]' 23 | * qui seront remplacées dans les processus lancés en parallèle par l'une des valeurs spécifiées. 24 | * @param int $iMax nombre maximal de processus lancés en parallèles 25 | * @return array liste de tableau associatif : array( 26 | * array( 27 | * 'value' => (string)"l'une des valeurs de $aValues", 28 | * 'error_code' => (int)code de retour Shell, 29 | * 'elapsed_time' => (int) temps approximatif en secondes, 30 | * 'cmd' => (string) commande shell exécutée, 31 | * 'output' => (string) sortie standard, 32 | * 'error' => (string) sortie d'erreur standard, 33 | * ), ... 34 | * ) 35 | * @throws RuntimeException si le moindre code de retour Shell non nul apparaît. 36 | * @throws RuntimeException si une valeur hors de $aValues apparaît dans les entrées 'value'. 37 | * @throws RuntimeException s'il manque des valeurs de $aValues dans le résultat final. 38 | */ 39 | public function parallelize (array $aValues, $sPattern, $iMax=DEPLOYMENT_PARALLELIZATION_MAX_NB_PROCESSES); 40 | 41 | /** 42 | * Exécute la commande shell spécifiée et retourne la sortie découpée par ligne dans un tableau. 43 | * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur. 44 | * 45 | * @param string $sCmd 46 | * @return array tableau indexé du flux de sortie shell découpé par ligne 47 | * @throws RuntimeException en cas d'erreur shell 48 | */ 49 | public function exec ($sCmd); 50 | 51 | /** 52 | * Exécute la commande shell spécifiée en l'encapsulant au besoin dans une connexion SSH 53 | * pour atteindre les hôtes distants. 54 | * 55 | * @param string $sPatternCmd commande au format printf 56 | * @param string $sParam paramètre du pattern $sPatternCmd, permettant en plus de décider si l'on 57 | * doit encapsuler la commande dans un SSH (si serveur distant) ou non. 58 | * @return array tableau indexé du flux de sortie shell découpé par ligne 59 | * @throws RuntimeException en cas d'erreur shell 60 | * @see isRemotePath() 61 | */ 62 | public function execSSH ($sPatternCmd, $sParam); 63 | 64 | /** 65 | * Retourne la commande Shell spécifiée envoyée à sprintf avec $sParam, 66 | * et encapsule au besoin le tout dans une connexion SSH 67 | * pour atteindre les hôtes distants (si $sParam est un hôte distant). 68 | * 69 | * @param string $sPatternCmd commande au format printf 70 | * @param string $sParam paramètre du pattern $sPatternCmd, permettant en plus de décider si l'on 71 | * doit encapsuler la commande dans un SSH (si serveur distant) ou non. 72 | * @return string la commande Shell spécifiée envoyée à sprintf avec $sParam, 73 | * et encapsule au besoin le tout dans une connexion SSH 74 | * pour atteindre les hôtes distants (si $sParam est un hôte distant). 75 | * @see isRemotePath() 76 | */ 77 | public function buildSSHCmd ($sPatternCmd, $sParam); 78 | 79 | /** 80 | * Retourne l'une des constantes de Shell_PathStatus, indiquant pour le chemin spécifié s'il est 81 | * inexistant, un fichier, un répertoire, un lien symbolique sur fichier ou encore un lien symbolique sur 82 | * répertoire. 83 | * 84 | * Les éventuels slash terminaux sont supprimés. 85 | * Si le statut est différent de inexistant, l'appel est mis en cache. 86 | * Un appel à remove() s'efforce de maintenir cohérent ce cache. 87 | * 88 | * Le chemin spécifié peut concerner un hôte distant (user@server:/path), auquel cas un appel SSH sera effectué. 89 | * 90 | * @param string $sPath chemin à tester, de la forme [user@server:]/path 91 | * @return int l'une des constantes de Shell_PathStatus 92 | * @throws RuntimeException en cas d'erreur shell 93 | * @see Shell_PathStatus 94 | * @see _aFileStatus 95 | */ 96 | public function getPathStatus ($sPath); 97 | 98 | /** 99 | * Pour chaque serveur retourne l'une des constantes de Shell_PathStatus, indiquant pour le chemin spécifié 100 | * s'il est inexistant, un fichier, un répertoire, un lien symbolique sur fichier 101 | * ou encore un lien symbolique sur répertoire. 102 | * 103 | * Comme getPathStatus(), mais sur une liste de serveurs. 104 | * 105 | * Les éventuels slash terminaux sont supprimés. 106 | * Si le statut est différent de inexistant, l'appel est mis en cache. 107 | * Un appel à remove() s'efforce de maintenir cohérent ce cache. 108 | * 109 | * @param string $sPath chemin à tester, sans mention de serveur 110 | * @param array $aServers liste de serveurs sur lesquels faire la demande de statut 111 | * @return array tableau associatif listant par serveur (clé) le status (valeur, constante de Shell_PathStatus) 112 | * @throws RuntimeException en cas d'erreur shell 113 | * @see getPathStatus() 114 | */ 115 | public function getParallelSSHPathStatus ($sPath, array $aServers); 116 | 117 | /** 118 | * Retourne un triplet dont la 1re valeur (bool) indique si le chemin spécifié commence par 119 | * '[user@]servername_or_ip:', la 2e (string) est le serveur (ou chaîne vide si $sPath est local), 120 | * et la 3e (string) est le chemin dépourvu de l'éventuel serveur. 121 | * 122 | * @param string $sPath chemin au format [[user@]servername_or_ip:]/path 123 | * @return array triplet dont la 1re valeur (bool) indique si le chemin spécifié commence par 124 | * '[user@]servername_or_ip:', la 2e (string) est le serveur (ou chaîne vide si $sPath est local), 125 | * et la 3e (string) est le chemin dépourvu de l'éventuel serveur. 126 | * @throws DomainException si syntaxe invalide (s'il reste des paramètres non résolus par exemple) 127 | */ 128 | public function isRemotePath ($sPath); 129 | 130 | /** 131 | * Copie un chemin vers un autre. 132 | * Les jokers '*' et '?' sont autorisés. 133 | * Par exemple copiera le contenu de $sSrcPath si celui-ci se termine par '/*'. 134 | * Si le chemin de destination n'existe pas, il sera créé. 135 | * 136 | * TODO ajouter gestion tar/gz 137 | * 138 | * @param string $sSrcPath chemin source, au format [[user@]hostname_or_ip:]/path 139 | * @param string $sDestPath chemin de destination, au format [[user@]hostname_or_ip:]/path 140 | * @param bool $bIsDestFile précise si le chemin de destination est un simple fichier ou non, 141 | * information nécessaire si l'on doit créer une partie de ce chemin si inexistant 142 | * @return array tableau indexé du flux de sortie shell découpé par ligne 143 | * @throws RuntimeException en cas d'erreur shell 144 | */ 145 | public function copy ($sSrcPath, $sDestPath, $bIsDestFile=false); 146 | 147 | /** 148 | * Crée un lien symbolique de chemin $sLinkPath vers la cible $sTargetPath. 149 | * 150 | * @param string $sLinkPath nom du lien, au format [[user@]hostname_or_ip:]/path 151 | * @param string $sTargetPath cible sur laquelle faire pointer le lien, au format [[user@]hostname_or_ip:]/path 152 | * @return array tableau indexé du flux de sortie shell découpé par ligne 153 | * @throws DomainException si les chemins référencent des serveurs différents 154 | * @throws RuntimeException en cas d'erreur shell 155 | */ 156 | public function createLink ($sLinkPath, $sTargetPath); 157 | 158 | /** 159 | * Entoure le chemin de guillemets doubles en tenant compte des jokers '*' et '?' qui ne les supportent pas. 160 | * Par exemple : '/a/b/img*jpg', donnera : '"/a/b/img"*"jpg"'. 161 | * Pour rappel, '*' vaut pour 0 à n caractères, '?' vaut pour exactement 1 caractère (et non 0 à 1). 162 | * 163 | * @param string $sPath 164 | * @return string 165 | */ 166 | public function escapePath ($sPath); 167 | 168 | /** 169 | * Supprime le chemin spécifié, répertoire ou fichier, distant ou local. 170 | * S'efforce de maintenir cohérent le cache de statut de chemins rempli par getPathStatus(). 171 | * 172 | * @param string $sPath chemin à supprimer, au format [[user@]hostname_or_ip:]/path 173 | * @return array tableau indexé du flux de sortie shell découpé par ligne 174 | * @throws DomainException si chemin invalide (garde-fou) 175 | * @throws RuntimeException en cas d'erreur shell 176 | * @see getPathStatus() 177 | */ 178 | public function remove ($sPath); 179 | 180 | /** 181 | * Effectue un tar gzip du répertoire $sSrcPath dans $sBackupPath. 182 | * 183 | * @param string $sSrcPath au format [[user@]hostname_or_ip:]/path 184 | * @param string $sBackupPath au format [[user@]hostname_or_ip:]/path 185 | * @return array tableau indexé du flux de sortie shell découpé par ligne 186 | * @throws RuntimeException en cas d'erreur shell 187 | */ 188 | public function backup ($sSrcPath, $sBackupPath); 189 | 190 | /** 191 | * Crée le chemin spécifié s'il n'existe pas déjà, avec les droits éventuellement transmis dans tous les cas. 192 | * 193 | * @param string $sPath chemin à créer, au format [[user@]hostname_or_ip:]/path 194 | * @param string $sMode droits utilisateur du chemin appliqués même si ce dernier existe déjà. 195 | * Par exemple '644'. 196 | * @return array tableau indexé du flux de sortie shell découpé par ligne 197 | * @throws RuntimeException en cas d'erreur shell 198 | */ 199 | public function mkdir ($sPath, $sMode=''); 200 | 201 | /** 202 | * Synchronise une source avec une ou plusieurs destinations. 203 | * 204 | * @param string $sSrcPath au format [[user@]hostname_or_ip:]/path 205 | * @param string|array $mDestPath chaque destination au format [[user@]hostname_or_ip:]/path 206 | * @param array $aValues liste de valeurs (string) optionnelles pour générer autant de demande de 207 | * synchronisation en parallèle. Dans ce cas ces valeurs viendront remplacer l'une après l'autre 208 | * les occurences de crochets vide '[]' présents dans $sSrcPath ou $sDestPath. 209 | * @param array $aIncludedPaths chemins à transmettre aux paramètres --include de la commande shell rsync. 210 | * Il précéderons les paramètres --exclude. 211 | * @param array $aExcludedPaths chemins à transmettre aux paramètres --exclude de la commande shell rsync 212 | * @return array tableau indexé du flux de sortie shell des commandes rsync exécutées, 213 | * découpé par ligne et analysé par _resumeSyncResult() 214 | * @throws RuntimeException en cas d'erreur shell 215 | * @throws RuntimeException car non implémenté quand plusieurs $mDestPath et $sSrcPath sont distants 216 | */ 217 | public function sync ($sSrcPath, $sDestPath, array $aValues=array(), 218 | array $aIncludedPaths=array(), array $aExcludedPaths=array()); 219 | } 220 | -------------------------------------------------------------------------------- /install/bash_completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # Bash completion support for twgit. 5 | # Dest path: /etc/bash_completion.d/twgit 6 | # Install: sudo make install 7 | # 8 | # 9 | # 10 | # Copyright (c) 2011 Twenga SA 11 | # Copyright (c) 2012-2013 Geoffroy Aubry 12 | # Copyright (c) 2013 Cyrille Hemidy 13 | # Copyright (c) 2014 Laurent Toussaint 14 | # 15 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 16 | # with the License. You may obtain a copy of the License at 17 | # 18 | # http://www.apache.org/licenses/LICENSE-2.0 19 | # 20 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 21 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 22 | # for the specific language governing permissions and limitations under the License. 23 | # 24 | # @copyright 2011 Twenga SA 25 | # @copyright 2012-2013 Geoffroy Aubry 26 | # @copyright 2013 Cyrille Hemidy 27 | # @copyright 2014 Laurent Toussaint 28 | # @license http://www.apache.org/licenses/LICENSE-2.0 29 | # 30 | 31 | 32 | 33 | function _twgit () { 34 | COMPREPLY=() 35 | local cur="${COMP_WORDS[COMP_CWORD]}" 36 | 37 | if [ "$COMP_CWORD" = "1" ]; then 38 | local opts="clean demo feature help hotfix init release tag update" 39 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 40 | 41 | elif [ "$COMP_CWORD" = "2" ]; then 42 | local command="${COMP_WORDS[COMP_CWORD-1]}" 43 | case "${command}" in 44 | feature) 45 | local opts="committers help list merge-into-hotfix merge-into-release migrate push remove start status what-changed" 46 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 47 | ;; 48 | demo) 49 | local opts="help list merge-demo merge-feature push remove start status update-features" 50 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 51 | ;; 52 | hotfix) 53 | local opts="finish help list push remove start" 54 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 55 | ;; 56 | release) 57 | local opts="committers finish help list merge-demo push remove reset start" 58 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 59 | ;; 60 | tag) 61 | local opts="help list" 62 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 63 | ;; 64 | esac 65 | 66 | elif [ "$COMP_CWORD" -gt "2" ]; then 67 | 68 | local words=( $(echo ${COMP_WORDS[@]} | sed 's/ -[a-zA-Z-]*//g' | sed "s/ ${cur}$//") ) 69 | local previous="${words[-1]}" 70 | local command="${COMP_WORDS[1]}" 71 | local action="${COMP_WORDS[2]}" 72 | local features="$( (git branch --no-color -r | grep 'feature-' | sed 's#^[* ]*origin/feature-##' | tr '\n' ' ') 2>/dev/null)" 73 | local demos="$( (git branch --no-color -r | grep 'demo-' | sed 's#^[* ]*origin/demo-##' | tr '\n' ' ') 2>/dev/null)" 74 | 75 | case "${command}" in 76 | feature) 77 | case "${action}" in 78 | committers) 79 | if [[ ${cur} == -* ]] ; then 80 | local opts="-F" 81 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 82 | elif [[ "${previous}" == "${action}" ]] ; then 83 | COMPREPLY=( $(compgen -W "${features}" -- ${cur}) ) 84 | fi 85 | ;; 86 | list) 87 | if [[ ${cur} == -* ]] ; then 88 | local opts="-F -c -x" 89 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 90 | fi 91 | ;; 92 | merge-into-release|merge-into-hotfix|remove|status|what-changed) 93 | if [[ "${previous}" == "${action}" ]] ; then 94 | COMPREPLY=( $(compgen -W "${features}" -- ${cur}) ) 95 | fi 96 | ;; 97 | migrate) 98 | if [[ ${cur} == -* ]] ; then 99 | local opts="-I" 100 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 101 | elif [[ "${previous}" == "${action}" ]] ; then 102 | local branches="$( (git branch --no-color -r | grep -vE 'origin/(feature-|demo-|release-|hotfix-|HEAD |stable$|master$)' | sed 's#^[* ]*origin/##' | tr '\n' ' ') 2>/dev/null)" 103 | COMPREPLY=( $(compgen -W "${branches}" -- ${cur}) ) 104 | fi 105 | ;; 106 | start) 107 | if [[ ${cur} == -* ]] ; then 108 | local opts="-d" 109 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 110 | else 111 | case "${previous}" in 112 | start|from-feature) 113 | COMPREPLY=( $(compgen -W "${features}" -- ${cur}) ) 114 | ;; 115 | from-demo) 116 | COMPREPLY=( $(compgen -W "${demos}" -- ${cur}) ) 117 | ;; 118 | *) 119 | if [[ ${words[-2]} == "${action}" && " ${features} " != *" ${previous} "* ]] ; then 120 | local opts="from-demo from-feature from-release" 121 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 122 | fi 123 | ;; 124 | esac 125 | fi 126 | ;; 127 | esac 128 | ;; 129 | 130 | demo) 131 | case "${action}" in 132 | list) 133 | if [[ ${cur} == -* ]] ; then 134 | local opts="-F -c" 135 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 136 | elif [[ "${previous}" == "${action}" ]] ; then 137 | COMPREPLY=( $(compgen -W "${demos}" -- ${cur}) ) 138 | fi 139 | ;; 140 | merge-demo|remove|status) 141 | if [[ "${previous}" == "${action}" ]] ; then 142 | COMPREPLY=( $(compgen -W "${demos}" -- ${cur}) ) 143 | fi 144 | ;; 145 | merge-feature) 146 | if [[ "${previous}" == "${action}" ]] ; then 147 | COMPREPLY=( $(compgen -W "${features}" -- ${cur}) ) 148 | fi 149 | ;; 150 | start) 151 | if [[ ${cur} == -* ]] ; then 152 | local opts="-d" 153 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 154 | else 155 | case "${previous}" in 156 | start|from-demo) 157 | COMPREPLY=( $(compgen -W "${demos}" -- ${cur}) ) 158 | ;; 159 | *) 160 | if [[ ${words[-2]} == "${action}" && " ${demos} " != *" ${previous} "* ]] ; then 161 | local opts="from-demo from-release" 162 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 163 | fi 164 | ;; 165 | esac 166 | fi 167 | ;; 168 | esac 169 | ;; 170 | 171 | hotfix) 172 | local hotfixes="$( (git branch --no-color -r | grep 'hotfix-' | sed 's#^[* ]*origin/hotfix-##' | tr '\n' ' ') 2>/dev/null)" 173 | case "${action}" in 174 | finish|start) 175 | if [[ ${cur} == -* ]] ; then 176 | local opts="-I" 177 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 178 | fi 179 | ;; 180 | list) 181 | if [[ ${cur} == -* ]] ; then 182 | local opts="-F" 183 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 184 | fi 185 | ;; 186 | remove) 187 | if [[ "${previous}" == "${action}" ]] ; then 188 | COMPREPLY=( $(compgen -W "${hotfixes}" -- ${cur}) ) 189 | fi 190 | ;; 191 | esac 192 | ;; 193 | 194 | release) 195 | local releases="$( (git branch --no-color -r | grep 'release-' | sed 's#^[* ]*origin/release-##' | tr '\n' ' ') 2>/dev/null)" 196 | case "${action}" in 197 | committers) 198 | if [[ ${cur} == -* ]] ; then 199 | local opts="-F" 200 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 201 | fi 202 | ;; 203 | finish) 204 | if [[ ${cur} == -* ]] ; then 205 | local opts="-I" 206 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 207 | fi 208 | ;; 209 | list) 210 | if [[ ${cur} == -* ]] ; then 211 | local opts="-F" 212 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 213 | fi 214 | ;; 215 | merge-demo) 216 | if [[ "${previous}" == "${action}" ]] ; then 217 | COMPREPLY=( $(compgen -W "${demos}" -- ${cur}) ) 218 | fi 219 | ;; 220 | remove) 221 | if [[ "${previous}" == "${action}" ]] ; then 222 | COMPREPLY=( $(compgen -W "${releases}" -- ${cur}) ) 223 | fi 224 | ;; 225 | reset) 226 | if [[ ${cur} == -* ]] ; then 227 | local opts="-I -M -m" 228 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 229 | elif [[ "${previous}" == "${action}" ]] ; then 230 | COMPREPLY=( $(compgen -W "${releases}" -- ${cur}) ) 231 | fi 232 | ;; 233 | start) 234 | if [[ ${cur} == -* ]] ; then 235 | local opts="-I -M -m" 236 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 237 | fi 238 | ;; 239 | esac 240 | ;; 241 | 242 | tag) 243 | case "${action}" in 244 | list) 245 | if [[ ${cur} == -* ]] ; then 246 | local opts="-F" 247 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 248 | fi 249 | ;; 250 | esac 251 | ;; 252 | esac 253 | fi 254 | } 255 | 256 | complete -F _twgit twgit 257 | -------------------------------------------------------------------------------- /inc/twgit_demo.inc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## 4 | # twgit 5 | # 6 | # 7 | # 8 | # Copyright (c) 2013 Geoffroy Aubry 9 | # Copyright (c) 2013 Cyrille Hemidy 10 | # Copyright (c) 2013 Sebastien Hanicotte 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 13 | # with the License. You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 18 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 19 | # for the specific language governing permissions and limitations under the License. 20 | # 21 | # @copyright 2013 Geoffroy Aubry 22 | # @copyright 2013 Cyrille Hemidy 23 | # @copyright 2013 Sebastien Hanicotte 24 | # @license http://www.apache.org/licenses/LICENSE-2.0 25 | # 26 | 27 | 28 | 29 | ## 30 | # Affiche l'aide de la commande demo. 31 | # 32 | function usage () { 33 | echo; CUI_displayMsg help 'Usage:' 34 | CUI_displayMsg help_detail 'twgit demo ' 35 | echo; CUI_displayMsg help 'Available actions are:' 36 | CUI_displayMsg help_detail 'list [] [-F]' 37 | CUI_displayMsg help_detail ' List remote demos with their merged features. If is'; 38 | CUI_displayMsg help_detail ' specified, then focus on this demo. Add -F to do not make fetch.'; echo 39 | CUI_displayMsg help_detail 'merge-demo ' 40 | CUI_displayMsg help_detail ' Try to merge specified demo into current demo.'; echo 41 | CUI_displayMsg help_detail 'merge-feature ' 42 | CUI_displayMsg help_detail ' Try to merge specified feature into current demo.'; echo 43 | CUI_displayMsg help_detail 'push' 44 | CUI_displayMsg help_detail " Push current demo to '$TWGIT_ORIGIN' repository." 45 | CUI_displayMsg help_detail " It's a shortcut for: \"git push $TWGIT_ORIGIN ${TWGIT_PREFIX_DEMO}…\""; echo 46 | CUI_displayMsg help_detail 'remove ' 47 | CUI_displayMsg help_detail ' Remove both local and remote specified demo branch. No feature will' 48 | CUI_displayMsg help_detail ' be removed.'; echo 49 | CUI_displayMsg help_detail 'start [from-release|from-demo ] [-d]' 50 | CUI_displayMsg help_detail ' Create both a new local and remote demo, or fetch the remote demo,' 51 | CUI_displayMsg help_detail ' or checkout the local demo. Add -d to delete beforehand local demo' 52 | CUI_displayMsg help_detail ' if exists.'; echo 53 | CUI_displayMsg help_detail 'status []' 54 | CUI_displayMsg help_detail ' Display information about specified demo: long name if a connector is' 55 | CUI_displayMsg help_detail ' set, last commit, status between local and remote demo and execute' 56 | CUI_displayMsg help_detail ' a git status if specified demo is the current branch.' 57 | CUI_displayMsg help_detail ' If no is specified, then use current demo.'; echo 58 | CUI_displayMsg help_detail 'update-features' 59 | CUI_displayMsg help_detail ' Try to update features into current demo.'; echo 60 | CUI_displayMsg help_detail "Prefix '$TWGIT_PREFIX_FEATURE' will be added to parameters." 61 | CUI_displayMsg help_detail "Prefix '$TWGIT_PREFIX_DEMO' will be added to parameters."; echo 62 | CUI_displayMsg help_detail '[help]' 63 | CUI_displayMsg help_detail ' Display this help.'; echo 64 | } 65 | 66 | ## 67 | # Action déclenchant l'affichage de l'aide. 68 | # 69 | function cmd_help () { 70 | usage; 71 | } 72 | 73 | ## 74 | # Liste les branches de demo. 75 | # Détaille les features incluses dans chaque branche demo. 76 | # Gère l'option '-F' permettant d'éviter le fetch. 77 | # Gère l'option '-c' compactant l'affichage en masquant les détails de commit auteur et date. 78 | # 79 | function cmd_list () { 80 | process_options "$@" 81 | require_parameter '-' 82 | clean_prefixes "$RETVAL" 'demo' 83 | local demo="$RETVAL" 84 | local demos 85 | 86 | process_fetch 'F' 87 | 88 | if [ -z "$demo" ]; then 89 | get_all_demos 90 | demos="$RETVAL" 91 | CUI_displayMsg help "Remote demos in progress:" 92 | else 93 | demos="$TWGIT_ORIGIN/$TWGIT_PREFIX_DEMO$demo" 94 | if ! has "$demos" $(get_remote_branches); then 95 | die "Remote demo '$demos' not found!" 96 | fi 97 | fi 98 | 99 | if [ ! -z "$demos" ]; then 100 | local add_empty_line=0 101 | local origin_prefix="$TWGIT_ORIGIN/" 102 | for d in $demos; do 103 | if ! isset_option 'c'; then 104 | [ "$add_empty_line" = "0" ] && add_empty_line=1 || echo 105 | fi 106 | display_super_branch 'demo' "${d:${#origin_prefix}}" 107 | done 108 | else 109 | display_branches 'demo' '' 110 | fi 111 | echo 112 | } 113 | 114 | ## 115 | # Push de la demo courante. 116 | # 117 | function cmd_push () { 118 | local current_branch=$(get_current_branch) 119 | get_all_demos 120 | local all_demos="$RETVAL" 121 | if ! has "$TWGIT_ORIGIN/$current_branch" $all_demos; then 122 | die "You must be in a demo to launch this command!" 123 | fi 124 | process_push_branch "$current_branch" 125 | } 126 | 127 | ## 128 | # Crée une nouvelle demo à partir du dernier tag, ou à partir de la demo demandée. 129 | # Gère l'option '-d' supprimant préalablement la demo locale, afin de forcer le recréation de la branche. 130 | # 131 | # @param string $1 nom court de la nouvelle demo. 132 | # 133 | function cmd_start () { 134 | process_options "$@" 135 | require_parameter 'demo' 136 | clean_prefixes "$RETVAL" 'demo' 137 | local demo="$RETVAL" 138 | parse_source_branch_info 'release' 'demo' 139 | local source_branch_info="$RETVAL" 140 | start_simple_branch "$demo" "$TWGIT_PREFIX_DEMO" ${source_branch_info} 141 | echo 142 | } 143 | 144 | ## 145 | # Suppression de la démo spécifiée. 146 | # 147 | # @param string $1 nom court de la démo à supprimer 148 | # 149 | function cmd_remove () { 150 | process_options "$@" 151 | require_parameter 'demo' 152 | clean_prefixes "$RETVAL" 'demo' 153 | local demo="$RETVAL" 154 | remove_demo "$demo" 155 | echo 156 | } 157 | 158 | ## 159 | # Try to merge specified feature into current demo. 160 | # 161 | # @param string $1 feature to merge in demo 162 | # 163 | function cmd_merge-feature () { 164 | process_options "$@" 165 | require_parameter 'feature' 166 | clean_prefixes "$RETVAL" 'feature' 167 | local feature="$RETVAL" 168 | local feature_fullname="$TWGIT_PREFIX_FEATURE$feature" 169 | 170 | # Tests préliminaires : 171 | assert_clean_working_tree 172 | process_fetch 173 | 174 | get_all_demos 175 | local all_demos="$RETVAL" 176 | local current_branch=$(get_current_branch) 177 | 178 | if ! has "$TWGIT_ORIGIN/$current_branch" $all_demos; then 179 | die "You must be in a demo!" 180 | fi 181 | 182 | merge_feature_into_branch "$feature" "$current_branch" 183 | } 184 | 185 | ## 186 | # Try to update features into current demo 187 | # 188 | # 189 | function cmd_update-features () { 190 | 191 | # Tests préliminaires : 192 | assert_clean_working_tree 193 | process_fetch 194 | 195 | get_all_demos 196 | local all_demos="$RETVAL" 197 | local current_branch=$(get_current_branch) 198 | 199 | if ! has "$TWGIT_ORIGIN/$current_branch" $all_demos; then 200 | die "You must be in a demo!" 201 | fi 202 | 203 | #Merge dernière release si nécessaire 204 | get_tags_not_merged_into_branch "$current_branch" 205 | local tags_not_merged="$GET_TAGS_NOT_MERGED_INTO_BRANCH_RETURN_VALUE" 206 | local nb_tags_no_merged="$(echo "$tags_not_merged" | wc -w)" 207 | 208 | if [ ! -z "$tags_not_merged" ]; then 209 | local msg='Tag' 210 | if echo "$tags_not_merged" | grep -q ' '; then 211 | msg="${msg}s" 212 | fi 213 | exec_git_command "git merge --no-ff $(get_last_tag)" 214 | msg="${msg} merged into this branch:" 215 | [ "$nb_tags_no_merged" -eq "$TWGIT_MAX_RETRIEVE_TAGS_NOT_MERGED" ] && msg="${msg} at least" 216 | msg="${msg} $(displayInterval "$tags_not_merged")." 217 | CUI_displayMsg warning "$msg" 218 | fi 219 | 220 | #Recuperation de la liste des features 221 | local demo_features=$(twgit demo list $current_branch -c -f | grep $TWGIT_PREFIX_FEATURE | awk -F$TWGIT_PREFIX_FEATURE '{print $2}' | cut -d " " -f1) 222 | 223 | CUI_displayMsg processing $demo_features 224 | 225 | # merge des features associées : 226 | for feature in $demo_features; do 227 | CUI_displayMsg processing "Update '$TWGIT_PREFIX_FEATURE$feature'" 228 | merge_feature_into_branch "$feature" "$current_branch" 229 | done 230 | 231 | } 232 | 233 | 234 | ## 235 | # Try to merge a specified demo and his features into demo 236 | # 237 | # @param string $1 nom de la demo 238 | # 239 | function cmd_merge-demo () { 240 | 241 | process_options "$@" 242 | require_parameter 'demo' 243 | clean_prefixes "$RETVAL" 'demo' 244 | local demo="$RETVAL" 245 | local demo_fullname="$TWGIT_ORIGIN/$TWGIT_PREFIX_DEMO$demo" 246 | 247 | # Tests préliminaires : 248 | assert_clean_working_tree 249 | process_fetch 250 | 251 | get_all_demos 252 | local all_demos="$RETVAL" 253 | local current_branch=$(get_current_branch) 254 | 255 | if ! has "$TWGIT_ORIGIN/$current_branch" $all_demos; then 256 | die "You must be in a demo!" 257 | fi 258 | 259 | # Merge de la demo dans la demo courante 260 | exec_git_command "git merge $demo_fullname" "Could not merge '$demo_fullname' into '$current_branch'!" 261 | 262 | #Recuperation de la liste des features 263 | local demo_features=$(twgit demo list $demo -c -f | grep $TWGIT_PREFIX_FEATURE | awk -F$TWGIT_PREFIX_FEATURE '{print $2}' | cut -d " " -f1) 264 | 265 | CUI_displayMsg processing $demo_features 266 | 267 | # merge des features associées : 268 | for feature in $demo_features; do 269 | CUI_displayMsg processing "Merge '$feature'" 270 | merge_feature_into_branch "$feature" "$current_branch" 271 | done 272 | 273 | } 274 | 275 | ## 276 | # Display information about specified demo: long name if a connector is 277 | # set, last commit, status between local and remote demo and execute 278 | # a git status if specified demo is the current branch. 279 | # If no is specified, then use current demo. 280 | # 281 | # @param string $1 optional demo, if empty then use current demo. 282 | # 283 | function cmd_status() { 284 | process_options "$@" 285 | require_parameter '-' 286 | clean_prefixes "$RETVAL" 'demo' 287 | local demo="$RETVAL" 288 | local current_branch=$(get_current_branch) 289 | 290 | # Si demo non spécifiée, récupérer la courante : 291 | local demo_fullname 292 | if [ -z "$demo" ]; then 293 | local all_demos=$(git branch -r | grep "$TWGIT_ORIGIN/$TWGIT_PREFIX_DEMO" | sed 's/^[* ]*//' | tr '\n' ' ' | sed 's/ *$//g') 294 | if ! has "$TWGIT_ORIGIN/$current_branch" $all_demos; then 295 | die "You must be in a demo if you didn't specify one!" 296 | fi 297 | demo_fullname="$current_branch" 298 | else 299 | demo_fullname="$TWGIT_PREFIX_DEMO$demo" 300 | if ! has $demo_fullname $(get_local_branches); then 301 | die "Local branch '$demo_fullname' does not exist and is required!" 302 | fi 303 | fi 304 | 305 | echo 306 | display_branches 'demo' "$TWGIT_ORIGIN/$demo_fullname" 307 | echo 308 | inform_about_branch_status $demo_fullname 309 | if [ "$demo_fullname" = "$current_branch" ]; then 310 | exec_git_command "git status" "Error while git status!" 311 | if [ "$(git config --get color.status)" != 'always' ]; then 312 | echo 313 | CUI_displayMsg help "Try this to get colored status in this command: git config --global color.status always" 314 | fi 315 | fi 316 | echo 317 | } 318 | -------------------------------------------------------------------------------- /tests/TwgitCommonAssertsTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Geoffroy Letournel 7 | */ 8 | class TwgitCommonAssertsTest extends TwgitTestCase 9 | { 10 | 11 | /** 12 | * @dataProvider providerTestAssertValidRefName 13 | * @shcovers inc/common.inc.sh::assert_valid_ref_name 14 | */ 15 | public function testAssertValidRefName ($sBranch, $sExpectedResult) 16 | { 17 | if ( ! empty($sExpectedResult)) { 18 | $this->setExpectedException('RuntimeException', $sExpectedResult); 19 | } 20 | $sMsg = $this->_localFunctionCall('assert_valid_ref_name "' . $sBranch . '"'); 21 | if (empty($sExpectedResult)) { 22 | $this->assertEquals('Check valid ref name...', $sMsg); 23 | } 24 | } 25 | 26 | public function providerTestAssertValidRefName () 27 | { 28 | $sErrorGitCheckRefMsg = ' is not a valid reference name! See git check-ref-format for more details.'; 29 | $sErrorPrefixMsg = "/!\ Unauthorized reference: '%s'! Pick another name without using any prefix" 30 | . " ('feature-', 'release-', 'hotfix-', 'demo-')."; 31 | 32 | return array( 33 | array('', $sErrorGitCheckRefMsg), 34 | array('a.', $sErrorGitCheckRefMsg), 35 | array('a/', $sErrorGitCheckRefMsg), 36 | array('a.lock', $sErrorGitCheckRefMsg), 37 | array('a..b', $sErrorGitCheckRefMsg), 38 | array('a~b', $sErrorGitCheckRefMsg), 39 | array('a^b', $sErrorGitCheckRefMsg), 40 | array('a:b', $sErrorGitCheckRefMsg), 41 | array('a?b', $sErrorGitCheckRefMsg), 42 | array('a*b', $sErrorGitCheckRefMsg), 43 | array('a[b', $sErrorGitCheckRefMsg), 44 | array('a\\b', $sErrorGitCheckRefMsg), 45 | array('a@{b', $sErrorGitCheckRefMsg), 46 | array('a b', $sErrorGitCheckRefMsg), 47 | 48 | array('feature-a', sprintf($sErrorPrefixMsg, 'feature-a')), 49 | array('xfeature-a', ''), 50 | array('release-a', sprintf($sErrorPrefixMsg, 'release-a')), 51 | array('xrelease-a', ''), 52 | array('hotfix-a', sprintf($sErrorPrefixMsg, 'hotfix-a')), 53 | array('xhotfix-a', ''), 54 | array('demo-a', sprintf($sErrorPrefixMsg, 'demo-a')), 55 | array('xdemo-a', ''), 56 | array('0.0.1', ''), 57 | ); 58 | } 59 | 60 | /** 61 | * @dataProvider providerTestAssertValidTagName 62 | * @shcovers inc/common.inc.sh::assert_valid_tag_name 63 | */ 64 | public function testAssertValidTagName ($sBranch, $sExpectedResult) 65 | { 66 | if ( ! empty($sExpectedResult)) { 67 | $this->setExpectedException('RuntimeException', $sExpectedResult); 68 | } 69 | $sMsg = $this->_localFunctionCall('assert_valid_tag_name "' . $sBranch . '"'); 70 | if (empty($sExpectedResult)) { 71 | $this->assertEquals("Check valid ref name...\nCheck valid tag name...", $sMsg); 72 | } 73 | } 74 | 75 | public function providerTestAssertValidTagName () 76 | { 77 | $sErrorGitCheckRefMsg = ' is not a valid reference name! See git check-ref-format for more details.'; 78 | $sErrorPrefixMsg = "/!\ Unauthorized reference: 'feature-a'! Pick another name without using any prefix" 79 | . " ('feature-', 'release-', 'hotfix-', 'demo-')."; 80 | $sErrorUnauthorizedMsg = 'Unauthorized tag name:'; 81 | 82 | return array( 83 | array('', $sErrorGitCheckRefMsg), 84 | array('a.', $sErrorGitCheckRefMsg), 85 | array('feature-a', $sErrorPrefixMsg), 86 | 87 | array('1', $sErrorUnauthorizedMsg), 88 | array('1.0', $sErrorUnauthorizedMsg), 89 | array('1.0.0.0', $sErrorUnauthorizedMsg), 90 | 91 | array('a.0.1', $sErrorUnauthorizedMsg), 92 | array('0.0.0', $sErrorUnauthorizedMsg), 93 | array('01.0.0', $sErrorUnauthorizedMsg), 94 | array('0.01.0', $sErrorUnauthorizedMsg), 95 | array('0.0.01', $sErrorUnauthorizedMsg), 96 | 97 | array('0.0.1', ''), 98 | array('0.1.0', ''), 99 | array('1.0.0', ''), 100 | array('10.10.10', ''), 101 | array('101.34.9', ''), 102 | ); 103 | } 104 | 105 | /** 106 | * @dataProvider providerTestAssertNewAndValidTagName 107 | * @shcovers inc/common.inc.sh::assert_new_and_valid_tag_name 108 | */ 109 | public function testAssertNewAndValidTagName ($sBranch, $sExpectedResult) 110 | { 111 | $this->_remoteExec('git init'); 112 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 113 | 114 | if ( ! empty($sExpectedResult)) { 115 | $this->setExpectedException('RuntimeException', $sExpectedResult); 116 | } 117 | $sMsg = $this->_localFunctionCall('assert_new_and_valid_tag_name "' . $sBranch . '"'); 118 | if (empty($sExpectedResult)) { 119 | $sExpectedMsg = "Check valid ref name...\nCheck valid tag name...\n" 120 | . "Check whether tag '$sBranch' already exists..."; 121 | $this->assertEquals($sExpectedMsg, $sMsg); 122 | } 123 | } 124 | 125 | public function providerTestAssertNewAndValidTagName () 126 | { 127 | $sErrorGitCheckRefMsg = ' is not a valid reference name! See git check-ref-format for more details.'; 128 | $sErrorPrefixMsg = "/!\ Unauthorized reference: 'feature-a'! Pick another name without using any prefix" 129 | . " ('feature-', 'release-', 'hotfix-', 'demo-')."; 130 | $sErrorUnauthorizedMsg = 'Unauthorized tag name:'; 131 | $sErrorAlreadyExistsMsg = "/!\ Tag 'v1.2.3' already exists! Try: twgit tag list"; 132 | 133 | return array( 134 | array('', $sErrorGitCheckRefMsg), 135 | array('a.', $sErrorGitCheckRefMsg), 136 | array('feature-a', $sErrorPrefixMsg), 137 | 138 | array('1.0', $sErrorUnauthorizedMsg), 139 | array('1.0.0.0', $sErrorUnauthorizedMsg), 140 | array('01.0.0', $sErrorUnauthorizedMsg), 141 | 142 | array('1.2.3', $sErrorAlreadyExistsMsg), 143 | array('1.2.2', ''), 144 | array('1.2.4', ''), 145 | array('101.34.9', ''), 146 | ); 147 | } 148 | 149 | /** 150 | * @shcovers inc/common.inc.sh::assert_tag_exists 151 | */ 152 | public function testAssertTagExists_ThrowExceptionWhenNoTag () 153 | { 154 | $this->_localExec('git init'); 155 | $this->setExpectedException('RuntimeException', "Get last tag...\n/!\ No tag exists!"); 156 | $sMsg = $this->_localFunctionCall('assert_tag_exists'); 157 | } 158 | 159 | /** 160 | * @shcovers inc/common.inc.sh::assert_tag_exists 161 | */ 162 | public function testAssertTagExists_WithOneTag () 163 | { 164 | $this->_remoteExec('git init'); 165 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 166 | 167 | $sMsg = $this->_localFunctionCall('assert_tag_exists'); 168 | $this->assertEquals("Get last tag...\nLast tag: v1.2.3", $sMsg); 169 | } 170 | 171 | /** 172 | * @shcovers inc/common.inc.sh::assert_tag_exists 173 | */ 174 | public function testAssertTagExists_WithMultipleTags () 175 | { 176 | $this->_remoteExec('git init'); 177 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 178 | $this->_localExec(TWGIT_EXEC . ' release start -I'); 179 | $this->_localExec(TWGIT_EXEC . ' release finish -I'); 180 | 181 | $sMsg = $this->_localFunctionCall('assert_tag_exists'); 182 | $this->assertEquals("Get last tag...\nLast tag: v1.3.0", $sMsg); 183 | } 184 | 185 | /** 186 | * @shcovers inc/common.inc.sh::assert_clean_working_tree 187 | */ 188 | public function testAssertCleanWorkingTree_WhenWorkingTreeEmpty () 189 | { 190 | $this->_localExec('rm .twgit && git init && git commit --allow-empty -m init'); 191 | $sMsg = $this->_localFunctionCall('assert_clean_working_tree'); 192 | $this->assertEquals("Check clean working tree...", $sMsg); 193 | } 194 | 195 | /** 196 | * @shcovers inc/common.inc.sh::assert_clean_working_tree 197 | */ 198 | public function testAssertCleanWorkingTree_ThrowExceptionWhenNewFile () 199 | { 200 | $this->_localExec('git init && git commit --allow-empty -m init'); 201 | $this->_localExec('touch a_file'); 202 | $this->setExpectedException( 203 | 'RuntimeException', 204 | "/!\ Untracked files or changes to be committed in your working tree!" 205 | ); 206 | $this->_localFunctionCall('assert_clean_working_tree'); 207 | } 208 | 209 | /** 210 | * @shcovers inc/common.inc.sh::assert_clean_working_tree 211 | */ 212 | public function testAssertCleanWorkingTree_ThrowExceptionWhenChangesToBeCommitted () 213 | { 214 | $this->_localExec('git init && git commit --allow-empty -m init'); 215 | $this->_localExec('touch a_file && git add .'); 216 | $this->setExpectedException( 217 | 'RuntimeException', 218 | "/!\ Untracked files or changes to be committed in your working tree!" 219 | ); 220 | $this->_localFunctionCall('assert_clean_working_tree'); 221 | } 222 | 223 | /** 224 | * @shcovers inc/common.inc.sh::assert_clean_working_tree 225 | */ 226 | public function testAssertCleanWorkingTree_AfterCommit () 227 | { 228 | $this->_localExec('git init && git commit --allow-empty -m init'); 229 | $this->_localExec('touch a_file && git add . && git commit -am comment'); 230 | $sMsg = $this->_localFunctionCall('assert_clean_working_tree'); 231 | $this->assertEquals("Check clean working tree...", $sMsg); 232 | } 233 | 234 | /** 235 | * @shcovers inc/common.inc.sh::assert_working_tree_is_not_on_delete_branch 236 | */ 237 | public function testAssertWorkingTreeIsNotOnDeleteBranch_WhenOnDeleteBranch () 238 | { 239 | $this->_remoteExec('git init'); 240 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 241 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2'); 242 | $this->_localExec('git checkout feature-1'); 243 | $sMsg = $this->_localFunctionCall('assert_working_tree_is_not_on_delete_branch feature-1'); 244 | $sExpectedMsg = 245 | "Check current branch...\n" 246 | . "Cannot delete the branch 'feature-1' which you are currently on! So:\n" 247 | . "git# git checkout " . self::STABLE . "\n" 248 | . "Switched to branch '" . self::STABLE . "'"; 249 | $this->assertContains($sExpectedMsg, $sMsg); 250 | } 251 | 252 | /** 253 | * @shcovers inc/common.inc.sh::assert_working_tree_is_not_on_delete_branch 254 | */ 255 | public function testAssertWorkingTreeIsNotOnDeleteBranch_WhenOK () 256 | { 257 | $this->_remoteExec('git init'); 258 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 259 | $this->_localExec(TWGIT_EXEC . ' feature start 1; ' . TWGIT_EXEC . ' feature start 2'); 260 | $this->_localExec('git checkout feature-2'); 261 | $sMsg = $this->_localFunctionCall('assert_working_tree_is_not_on_delete_branch feature-1'); 262 | $this->assertNotContains("Cannot delete the branch 'feature-1' which you are currently on!", $sMsg); 263 | } 264 | 265 | /** 266 | * @shcovers inc/common.inc.sh::assert_remote_branch_exists 267 | */ 268 | public function testAssertRemoteBranchExists_WhenKo () 269 | { 270 | $this->_remoteExec('git init'); 271 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 272 | $this->_localExec(TWGIT_EXEC . ' feature start 2'); 273 | 274 | $this->setExpectedException('\RuntimeException', "Remote branch '" . self::_remote('feature-1') . "' not found!"); 275 | $this->_localFunctionCall('assert_remote_branch_exists feature-1'); 276 | } 277 | 278 | /** 279 | * @shcovers inc/common.inc.sh::assert_remote_branch_exists 280 | */ 281 | public function testAssertRemoteBranchExists_WhenOk () 282 | { 283 | $this->_remoteExec('git init'); 284 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 285 | $this->_localExec(TWGIT_EXEC . ' feature start 1'); 286 | $this->_localExec(TWGIT_EXEC . ' feature start 2'); 287 | $sMsg = $this->_localFunctionCall('assert_remote_branch_exists feature-1'); 288 | $this->assertNotContains("Remote branch '" . self::_remote('feature-1') . "' not found!", $sMsg); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /tests/TwgitHotfixTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @author Geoffroy Letournel 7 | * @author Sebastien Hanicotte 8 | */ 9 | class TwgitHotfixTest extends TwgitTestCase 10 | { 11 | 12 | /** 13 | */ 14 | public function testStart_WithAmbiguousRef () 15 | { 16 | $this->_remoteExec('git init'); 17 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 18 | $this->_localExec('git branch v1.2.3 v1.2.3'); 19 | 20 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 21 | $this->assertNotContains("warning: refname 'v1.2.3' is ambiguous.", $sMsg); 22 | $this->assertNotContains("fatal: Ambiguous object name: 'v1.2.3'.", $sMsg); 23 | } 24 | 25 | /** 26 | */ 27 | public function testStart_WithFullColoredGit () 28 | { 29 | $this->_remoteExec('git init'); 30 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 31 | 32 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 33 | $this->_localExec( 34 | "git config color.branch always\n" 35 | . "git config color.diff always\n" 36 | . "git config color.interactive always\n" 37 | . "git config color.status always\n" 38 | . "git config color.ui always\n" 39 | ); 40 | 41 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix start'); 42 | $sExpected = "(i) Local branch 'hotfix-1.2.4' up-to-date with remote '" . self::_remote('hotfix-1.2.4') . "'."; 43 | $this->assertContains($sExpected, $sMsg); 44 | } 45 | 46 | /** 47 | * @shcovers inc/common.inc.sh::assert_clean_stable_branch_and_checkout 48 | */ 49 | public function testFinish_ThrowExceptionWhenExtraCommitIntoStable () 50 | { 51 | $this->_remoteExec('git init'); 52 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 53 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 54 | 55 | $this->_localExec('git checkout ' . self::STABLE); 56 | $this->_localExec('git commit --allow-empty -m "extra commit!"'); 57 | 58 | $this->setExpectedException( 59 | 'RuntimeException', 60 | "Local '" . self::STABLE . "' branch is ahead of '" . self::$_remoteStable . "'! Commits on '" . self::STABLE . "' are out of process." 61 | . " Try: git checkout " . self::STABLE . " && git reset " . self::$_remoteStable 62 | ); 63 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix finish'); 64 | } 65 | 66 | /** 67 | * @shcovers inc/common.inc.sh::assert_clean_stable_branch_and_checkout 68 | */ 69 | public function testFinish_WithExtraCommitIntoStableThenReset () 70 | { 71 | $this->_remoteExec('git init'); 72 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 73 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 74 | 75 | $this->_localExec('git checkout ' . self::STABLE); 76 | $this->_localExec('git commit --allow-empty -m "extra commit!"'); 77 | $this->_localExec('git checkout ' . self::STABLE . ' && git reset ' . self::$_remoteStable); 78 | 79 | $this->_localExec(TWGIT_EXEC . ' hotfix finish -I'); 80 | $sMsg = $this->_localExec('git tag'); 81 | $this->assertContains('v1.2.4', $sMsg); 82 | } 83 | 84 | /** 85 | */ 86 | public function testFinish_WithEmptyHotfix () 87 | { 88 | $this->_remoteExec('git init'); 89 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 90 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 91 | $this->_localExec(TWGIT_EXEC . ' hotfix finish -I'); 92 | $sMsg = $this->_localExec('git tag'); 93 | $this->assertContains('v1.2.4', $sMsg); 94 | } 95 | 96 | /** 97 | * @shcovers inc/common.inc.sh::is_initial_author 98 | */ 99 | public function testStart_WithExistentHotfixSameAuthor () 100 | { 101 | $this->_remoteExec('git init'); 102 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 103 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 104 | $this->_localExec('git checkout $TWGIT_STABLE'); 105 | 106 | $userName = $this->_localExec('git config user.name'); 107 | $userEmail = $this->_localExec('git config user.email'); 108 | 109 | $sResult = $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 110 | $sExpected = "Remote hotfix '" . self::ORIGIN . "/hotfix-1.2.4' was started by $userName <$userEmail>."; 111 | 112 | $this->assertContains("Check initial author...", $sResult); 113 | $this->assertNotContains($sExpected, $sResult); 114 | } 115 | 116 | /** 117 | * @shcovers inc/common.inc.sh::is_initial_author 118 | */ 119 | public function testStart_WithExistentHotfixOtherAuthor () 120 | { 121 | $this->_remoteExec('git init'); 122 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 123 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 124 | $this->_localExec('git checkout $TWGIT_STABLE'); 125 | 126 | $userName = $this->_localExec('git config user.name'); 127 | $userEmail = $this->_localExec('git config user.email'); 128 | 129 | $this->_localExec("git config --local user.name 'Other Name'"); 130 | $this->_localExec("git config --local user.email 'other@email.com'"); 131 | 132 | $sResult = $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 133 | $sExpected = "Remote hotfix '" . self::ORIGIN . "/hotfix-1.2.4' was started by $userName <$userEmail>."; 134 | 135 | $this->_localExec("git config --local --unset user.name"); 136 | $this->_localExec("git config --local --unset user.email"); 137 | 138 | $this->assertContains("Check initial author...", $sResult); 139 | $this->assertContains($sExpected, $sResult); 140 | } 141 | 142 | /** 143 | * @shcovers inc/common.inc.sh::assert_clean_stable_branch_and_checkout 144 | */ 145 | public function testRemove_ThrowExceptionWhenExtraCommitIntoStable () 146 | { 147 | $this->_remoteExec('git init'); 148 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 149 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 150 | 151 | $this->_localExec('git checkout ' . self::STABLE); 152 | $this->_localExec('git commit --allow-empty -m "extra commit!"'); 153 | 154 | $this->setExpectedException( 155 | 'RuntimeException', 156 | "Local '" . self::STABLE . "' branch is ahead of '" . self::$_remoteStable . "'! Commits on '" . self::STABLE . "' are out of process." 157 | . " Try: git checkout " . self::STABLE . " && git reset " . self::$_remoteStable 158 | ); 159 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix remove 1.2.4'); 160 | } 161 | 162 | public function testRemove_ThrowExceptionWhenExtraCommitIntoStableWithPrefixes () 163 | { 164 | $this->_remoteExec('git init'); 165 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 166 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 167 | 168 | $this->_localExec('git checkout ' . self::STABLE); 169 | $this->_localExec('git commit --allow-empty -m "extra commit!"'); 170 | 171 | $this->setExpectedException( 172 | 'RuntimeException', 173 | "Local '" . self::STABLE . "' branch is ahead of '" . self::$_remoteStable . "'!" 174 | . " Commits on '" . self::STABLE . "' are out of process." 175 | . " Try: git checkout " . self::STABLE . " && git reset " . self::$_remoteStable 176 | ); 177 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix remove hotfix-1.2.4'); 178 | $this->assertContains("Assume hotfix was '1.2.4' instead of 'hotfix-1.2.4'", $sMsg); 179 | } 180 | 181 | /** 182 | * @shcovers inc/common.inc.sh::assert_clean_stable_branch_and_checkout 183 | */ 184 | public function testRemove_WithExtraCommitIntoStableThenReset () 185 | { 186 | $this->_remoteExec('git init'); 187 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 188 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 189 | 190 | $this->_localExec('git checkout ' . self::STABLE); 191 | $this->_localExec('git commit --allow-empty -m "extra commit!"'); 192 | $this->_localExec('git checkout ' . self::STABLE . ' && git reset ' . self::$_remoteStable); 193 | 194 | $this->_localExec(TWGIT_EXEC . ' hotfix remove 1.2.4'); 195 | $sMsg = $this->_localExec('git tag'); 196 | $this->assertContains('v1.2.4', $sMsg); 197 | } 198 | 199 | /** 200 | * @shcovers inc/common.inc.sh::update_version_information 201 | */ 202 | public function testStartWithVersionInfo () 203 | { 204 | $this->_remoteExec('git init'); 205 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 206 | $this->_localExec(TWGIT_EXEC . ' feature start 42'); 207 | $this->_localExec('echo "TWGIT_VERSION_INFO_PATH=\'not_exists,csv_tags\'" >> .twgit'); 208 | $this->_localExec('cp ' . TWGIT_TESTS_DIR . '/resources/csv_tags csv_tags'); 209 | $this->_localExec('git add .'); 210 | $this->_localExec('git commit -m "Adding testing files"'); 211 | $this->_localExec(TWGIT_EXEC . ' release start -I'); 212 | $this->_localExec(TWGIT_EXEC . ' feature merge-into-release 42'); 213 | $this->_localExec(TWGIT_EXEC . ' release finish -I'); 214 | $this->_localExec(TWGIT_EXEC . ' hotfix start -I'); 215 | $sResult = $this->_localExec('cat csv_tags'); 216 | $sExpected = "\$Id:1.3.1\$\n" 217 | . "-------\n" 218 | . "\$Id:1.3.1\$\n" 219 | . "-------\n" 220 | . "\$id\$\n" 221 | . "-------\n" 222 | . "\$Id:1.3.1\$ \$Id:1.3.1\$"; 223 | $this->assertEquals($sExpected, $sResult); 224 | } 225 | 226 | /** 227 | * @dataProvider providerTestListAboutBranchesOutOfProcess 228 | */ 229 | public function testList_AboutBranchesOutOfProcess ($sLocalCmd, $sExpectedContent, $sNotExpectedContent) 230 | { 231 | $this->_remoteExec('git init && git commit --allow-empty -m "-" && git checkout -b feature-currentOfNonBareRepo'); 232 | $this->_localExec(TWGIT_EXEC . ' init 1.2.3 ' . TWGIT_REPOSITORY_ORIGIN_DIR); 233 | $this->_localExec('cd ' . TWGIT_REPOSITORY_SECOND_REMOTE_DIR . ' && git init'); 234 | $this->_localExec('git remote add second ' . TWGIT_REPOSITORY_SECOND_REMOTE_DIR); 235 | 236 | $this->_localExec($sLocalCmd); 237 | $sMsg = $this->_localExec(TWGIT_EXEC . ' hotfix list'); 238 | if ( ! empty($sExpectedContent)) { 239 | $this->assertContains($sExpectedContent, $sMsg); 240 | } 241 | if ( ! empty($sNotExpectedContent)) { 242 | $this->assertNotContains($sNotExpectedContent, $sMsg); 243 | } 244 | } 245 | 246 | public function providerTestListAboutBranchesOutOfProcess () 247 | { 248 | return array( 249 | array(':', '', 'Following branches are out of process'), 250 | array(':', '', 'Following local branches are ambiguous'), 251 | array( 252 | 'git checkout -b feature-X && git push ' . self::ORIGIN . ' feature-X' 253 | . ' && git checkout -b release-X && git push ' . self::ORIGIN . ' release-X' 254 | . ' && git checkout -b hotfix-X && git push ' . self::ORIGIN . ' hotfix-X' 255 | . ' && git checkout -b demo-X && git push ' . self::ORIGIN . ' demo-X' 256 | . ' && git checkout -b master && git push ' . self::ORIGIN . ' master' 257 | . ' && git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess' 258 | . ' && git remote set-head ' . self::ORIGIN . ' ' . self::STABLE, 259 | "/!\ Following branches are out of process: '" . self::_remote('outofprocess') . "'!", 260 | 'Following local branches are ambiguous' 261 | ), 262 | array( 263 | 'git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess && git push second outofprocess' 264 | . ' && git checkout -b out2 && git push ' . self::ORIGIN . ' out2 && git push second out2', 265 | "/!\ Following branches are out of process: '" . self::_remote('out2') . "', '" . self::_remote('outofprocess') . "'!", 266 | 'Following local branches are ambiguous' 267 | ), 268 | array( 269 | 'git branch v1.2.3 v1.2.3', 270 | "/!\ Following local branches are ambiguous: 'v1.2.3'!", 271 | 'Following branches are out of process' 272 | ), 273 | array( 274 | 'git checkout -b outofprocess && git push ' . self::ORIGIN . ' outofprocess && git branch v1.2.3 v1.2.3', 275 | "/!\ Following branches are out of process: '" . self::_remote('outofprocess') . "'!\n" 276 | . "/!\ Following local branches are ambiguous: 'v1.2.3'!", 277 | '' 278 | ), 279 | ); 280 | } 281 | } 282 | --------------------------------------------------------------------------------