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 |
--------------------------------------------------------------------------------