# $Id$ # vim:et:ft=sh:sts=2:sw=2 # # git-flow -- A collection of Git extensions to provide high-level # repository operations for Vincent Driessen's branching model. # # A blog post presenting this model is found at: # http://blog.avirtualhome.com/development-workflow-using-git/ # # Feel free to contribute to this project at: # http://github.com/petervanderdoes/gitflow # # Authors: # Copyright 2012-2019 Peter van der Does. All rights reserved. # # Original Author: # Copyright 2010 Vincent Driessen. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Common functionality # # Shell output warn() { echo "$@" >&2; } die() { warn "Fatal: $@"; exit 1; } die_help() { warn $@; flags_help; exit 1; } escape() { echo "$1" | sed 's/\([\.\$\*]\)/\\\1/g' } # # String contains function # $1 haystack # $2 Needle # contains() { local return case $1 in *$2*) return=$FLAGS_TRUE ;; *) return=$FLAGS_FALSE ;; esac return $return } # Basic math min() { [ "$1" -le "$2" ] && echo "$1" || echo "$2"; } max() { [ "$1" -ge "$2" ] && echo "$1" || echo "$2"; } # Basic string matching startswith() { [ "$1" != "${1#$2}" ]; } endswith() { [ "$1" != "${1%$2}" ]; } # Convenience functions for checking shFlags flags flag() { local FLAG; eval FLAG='$FLAGS_'$1; [ $FLAG -eq $FLAGS_TRUE ]; } noflag() { local FLAG; eval FLAG='$FLAGS_'$1; [ $FLAG -ne $FLAGS_TRUE ]; } # check_boolean # Check if given value can be interpreted as a boolean # # This function determines if the passed parameter is a valid boolean value. # # Param $1: string Value to check if it's a valid boolean # # Return: string FLAGS_TRUE|FLAGS_FALSE|FLAGS_ERROR # FLAGS_TRUE if the parameter is a boolean TRUE # FLAGS_FALSE if the parameter is a boolean FALSE # FLAGS_ERROR if the parameter is not a boolean # check_boolean() { local _return _value _value="${1}" case "${_value}" in ${FLAGS_TRUE} | [yY] | [yY][eE][sS] | [tT] | [tT][rR][uU][eE]) _return=${FLAGS_TRUE} ;; ${FLAGS_FALSE} | [nN] | [nN][oO] | [fF] | [fF][aA][lL][sS][eE]) _return=${FLAGS_FALSE} ;; *) _return=${FLAGS_ERROR} ;; esac unset _value return ${_return} } # # Git specific common functionality # git_local_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/heads; } git_remote_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/remotes; } git_all_branches() { git for-each-ref --sort refname --format='%(refname:short)' refs/remotes refs/heads; } git_all_tags() { git for-each-ref --format='%(refname:short)' refs/tags; } git_local_branches_prefixed() { [ -z $1 ] && die "Prefix parameter missing." # This should never happen. git for-each-ref --format='%(refname:short)' refs/heads/$1\* ; } git_current_branch() { local branch_name branch_name="$(git symbolic-ref --quiet HEAD)" [ -z $branch_name ] && branch_name="(unnamed branch)" || branch_name="$(git for-each-ref --format='%(refname:short)' $branch_name)" echo "$branch_name" } git_is_clean_working_tree() { git rev-parse --verify HEAD >/dev/null || exit 1 git update-index -q --ignore-submodules --refresh # Check for unstaged changes git diff-files --quiet --ignore-submodules || return 1 # Check for Uncommited changes git diff-index --cached --quiet --ignore-submodules HEAD -- || return 2 return 0 } git_repo_is_headless() { ! git rev-parse --quiet --verify HEAD >/dev/null 2>&1 } git_local_branch_exists() { [ -n "$1" ] || die "Missing branch name" [ -n "$(git for-each-ref --format='%(refname:short)' refs/heads/$1)" ] } git_remote_branch_exists() { [ -n "$1" ] || die "Missing branch name" [ -n "$(git for-each-ref --format='%(refname:short)' refs/remotes/$1)" ] } git_remote_branch_delete() { [ -n "$1" ] || die "Missing branch name" if git_remote_branch_exists "$ORIGIN/$1"; then git_do push "$ORIGIN" :"$1" || die "Could not delete the remote $1 in $ORIGIN." return 0 else warn "Trying to delete the remote branch $1, but it does not exists in $ORIGIN" return 1 fi } git_branch_exists() { [ -n "$1" ] || die "Missing branch name" git_local_branch_exists "$1" || git_remote_branch_exists "$ORIGIN/$1" } git_tag_exists() { [ -n "$1" ] || die "Missing tag name" [ -n "$(git for-each-ref --format='%(refname:short)' refs/tags/$1)" ] } git_config_bool_exists() { local value [ -n "$1" ] || die "Missing config option" value=$(git config --get --bool $1) [ "$value" = "true" ] } # # git_compare_refs() # # Tests whether two references have diverged and need merging # first. It returns error codes to provide more detail, like so: # # 0 References point to the same commit # 1 First given reference needs fast-forwarding # 2 Second given reference needs fast-forwarding # 3 References need a real merge # 4 There is no merge base, i.e. the references have no common ancestors # git_compare_refs() { local commit1 commit2 base commit1=$(git rev-parse "$1"^{}) commit2=$(git rev-parse "$2"^{}) if [ "$commit1" != "$commit2" ]; then base=$(git merge-base "$commit1" "$commit2") if [ $? -ne 0 ]; then return 4 elif [ "$commit1" = "$base" ]; then return 1 elif [ "$commit2" = "$base" ]; then return 2 else return 3 fi else return 0 fi } # # git_is_branch_merged_into() # # Checks whether branch $1 is successfully merged into $2 # git_is_branch_merged_into() { local merge_hash base_hash merge_hash=$(git merge-base "$1"^{} "$2"^{}) base_hash=$(git rev-parse "$1"^{}) # If the hashes are equal, the branches are merged. [ "$merge_hash" = "$base_hash" ] } # # git_is_ancestor() # # This is the same function as git_is_branch_merged_into but # for readability given a different name. # git_is_ancestor() { git_is_branch_merged_into "$1" "$2" } # # git_fetch_branch() # # $1 Origin - Where to fetch from # $2 Branch - Which branch to fetch # # This fetches the given branch from the given origin. # Instead of storing it in FETCH_HEAD it will be stored in # refs/remotes// # git_fetch_branch() { local origin branch [ -n "$1" ] || die "Missing origin" [ -n "$2" ] || die "Missing branch name" origin="$1" branch="$2" if git_remote_branch_exists "$origin/$branch"; then git_do fetch -q "$origin" "$branch" || die "Could not fetch $branch from $origin." else warn "Trying to fetch branch '$origin/$branch' but it does not exist." fi } # # gitflow specific common functionality # # Function used to check if the repository is git-flow enabled. gitflow_has_master_configured() { local master master=$(git config --get gitflow.branch.master) [ "$master" != "" ] && git_local_branch_exists "$master" } gitflow_has_develop_configured() { local develop develop=$(git config --get gitflow.branch.develop) [ "$develop" != "" ] && git_local_branch_exists "$develop" } gitflow_is_initialized() { gitflow_has_master_configured && \ gitflow_has_develop_configured && \ [ "$(git config --get gitflow.branch.master)" != "$(git config --get gitflow.branch.develop)" ] && \ git config --get-regexp gitflow.prefix >/dev/null 2>&1 } # Loading settings that can be overridden using git config gitflow_load_settings() { export GIT_CURRENT_REPO_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" DOT_GIT_DIR=$(git rev-parse --git-dir) export DOT_GIT_DIR="$(cd ${DOT_GIT_DIR} >/dev/null 2>&1 && pwd)" export HOOKS_DIR="$(git config --get gitflow.path.hooks || echo ${DOT_GIT_DIR}/hooks)" # the second option is used to support previous versions of git-flow export MASTER_BRANCH=$(git config --get gitflow.branch.master) export DEVELOP_BRANCH=$(git config --get gitflow.branch.develop) export ORIGIN=$(git config --get gitflow.origin || echo origin) GITFLOW_CONFIG="$DOT_GIT_DIR/gitflow_config" if [ -f "$GITFLOW_CONFIG" ]; then # move all settings from old .git/gitflow_config to the local conf. warn "Migrating old \"$GITFLOW_CONFIG\" to the \"--local\" repo config." _config_lines=`git config --list --file="$GITFLOW_CONFIG"`; for _config_line in ${_config_lines}; do _key=${_config_line%=*} _value=${_config_line#=*} git_do config --local gitflow.${_key} ${_value} done; mv "$GITFLOW_CONFIG" "$GITFLOW_CONFIG".backup 2>/dev/null fi } # # gitflow_resolve_nameprefix # # Inputs: # $1 = name prefix to resolve # $2 = branch prefix to use # # Searches branch names from git_local_branches() to look for a unique # branch name whose name starts with the given name prefix. # # There are multiple exit codes possible: # 0: The unambiguous full name of the branch is written to stdout # (success) # 1: No match is found. # 2: Multiple matches found. These matches are written to stderr # gitflow_resolve_nameprefix() { local name prefix local match matches num_matches name=$1 prefix=$2 # first, check if there is a perfect match if git_local_branch_exists "$prefix$name"; then echo "$name" return 0 fi matches=$(echo "$(git_local_branches)" | grep "^$(escape "$prefix$name")") num_matches=$(echo "$matches" | wc -l) if [ -z "$matches" ]; then # no prefix match, so take it literally warn "No branches match '$prefix$name*'" return 1 else if [ $num_matches -eq 1 ]; then echo "${matches#$prefix}" return 0 else # multiple matches, cannot decide warn "Multiple branches match prefix '$name':" for match in $matches; do warn "- $match" done return 2 fi fi } # # Check if the given branch is a git-flow branch # gitflow_is_prefixed_branch() { local branch return branch=$1 case $branch in $(git config --get gitflow.prefix.feature)* | \ $(git config --get gitflow.prefix.bugfix)* | \ $(git config --get gitflow.prefix.release)* | \ $(git config --get gitflow.prefix.hotfix)* | \ $(git config --get gitflow.prefix.support)* ) return=0 ;; *) return=1 ;; esac return $return } # # Update the config with the base of a new git-flow branch. # # @param $1 Base of the new branch # @param $2 Name of the branch # gitflow_config_set_base_branch() { local base branch base=$1 branch=$2 $(git_do config --local "gitflow.branch.$branch.base" $base) } # # Get the base of a branch as set by gitflow_set_branch # # @param $1 Name of the branch # @return string|empty String when a base is found otherwise empty # gitflow_config_get_base_branch() { local branch branch=$1 echo $(git config --local --get "gitflow.branch.$branch.base") } # # Remove the section that contains the base of a branch as set by gitflow_set_branch # # @param $1 Name of the branch # gitflow_config_remove_base_section() { local branch branch=$1 $(git_do config --local --remove-section "gitflow.branch.$branch" 2>/dev/null) } # # Remove the base of the git-flow branch from the. # @param $1 Name of the branch # gitflow_config_remove_base_branch() { local base base=$1 $(git_do config --local --unset "gitflow.branch.$branch.base" 2>/dev/null) } # # Remove the base of the git-flow branch from the. # @param $1 Name of the branch # gitflow_config_rename_sections() { local new local old old=$1 new=$2 $(git_do config --local --rename-section "gitflow.branch.$old" "gitflow.branch.$new" 2>/dev/null) } # gitflow_override_flag_boolean() # # Override a boolean flag # # Param $1: string The name of the config variable e.g. "feature.start.fetch" # Param $2: string The flag name # gitflow_override_flag_boolean() { local _variable _variable=$(git config --bool --get gitflow.$1 2>&1) case $? in 0) [ "${_variable}" = "true" ] && eval "FLAGS_${2}=${FLAGS_TRUE}" || eval "FLAGS_${2}=${FLAGS_FALSE}" ;; 128) die "${_variable}" ;; esac unset _variable return ${FLAGS_TRUE} } # gitflow_override_flag_string() # # Override a string flag # # Param $1: string The name of the config variable e.g. "feature.start.fetch" # Param $2: string The flag name # gitflow_override_flag_string() { local _variable _variable=$(git config --get gitflow.$1 2>&1) case $? in 0) eval "FLAGS_${2}=\"${_variable}\"" ;; esac unset _variable return ${FLAGS_TRUE} } # gitflow_create_squash_message() # # Create the squash message, overriding the one generated by git itself # # Param $1: string The line to be added # Param $2: string The base of the branch that will me merged # Param $3: string The branch that will be merged. # gitflow_create_squash_message() { echo Squashed commit of the following: echo echo $1 echo git log --no-merges --pretty=medium ^"$2" $3 } # # Parameter functions # gitflow_require_name_arg() { if [ "$NAME" = "" ]; then die_help "Missing argument " fi } gitflow_expand_nameprefix_arg() { local expanded_name exitcode gitflow_require_name_arg expanded_name=$(gitflow_resolve_nameprefix "$NAME" "$PREFIX") exitcode=$? case $exitcode in 0) NAME=$expanded_name BRANCH=$PREFIX$NAME ;; *) exit 1 ;; esac } gitflow_require_version_arg() { if [ "$VERSION" = "" ]; then die_help "Missing argument " fi } gitflow_expand_versionprefix_arg() { local expanded_version exitcode gitflow_require_version_arg version=$(gitflow_resolve_nameprefix "$VERSION" "$PREFIX") exitcode=$? case $exitcode in 0) VERSION=$version BRANCH=$PREFIX$VERSION ;; *) exit 1 ;; esac } gitflow_require_base_arg() { if [ "$BASE" = "" ]; then die_help "Missing argument " fi } gitflow_use_current_branch_name() { local current_branch current_branch=$(git_current_branch) if startswith "$current_branch" "$PREFIX"; then BRANCH=$current_branch NAME=${BRANCH#$PREFIX} else warn "The current HEAD is no ${SUBCOMMAND} branch." warn "Please specify a argument." exit 1 fi } gitflow_use_current_branch_version() { local current_branch current_branch=$(git_current_branch) if startswith "$current_branch" "$PREFIX"; then BRANCH=$current_branch VERSION=${BRANCH#$PREFIX} else warn "The current HEAD is no ${SUBCOMMAND} branch." warn "Please specify a argument." exit 1 fi } gitflow_rename_branch() { # Parse arguments FLAGS "$@" || exit $? eval set -- "${FLAGS_ARGV}" # read arguments into global variables if [ -z $1 ]; then NEW_NAME='' else NEW_NAME=$1 fi if [ -z $2 ]; then NAME='' else NAME=$2 fi BRANCH=${PREFIX}${NAME} NEW_BRANCH=${PREFIX}${NEW_NAME} if [ -z "$NEW_NAME" ]; then die "No new name given." fi # Use current branch if no name is given if [ "$NAME" = "" ]; then gitflow_use_current_branch_name fi # Sanity checks require_branch "$BRANCH" require_branch_absent "$NEW_BRANCH" run_pre_hook "$NAME" "$ORIGIN" "$BRANCH" git_do branch -m "$BRANCH" "$NEW_BRANCH" || die "Error renaming branch '$BRANCH' to '$NEW_BRANCH'" gitflow_config_rename_sections "$BRANCH" "$NEW_BRANCH" run_post_hook "$NAME" "$ORIGIN" "$BRANCH" echo echo "Summary of actions:" echo "- Branch '$BRANCH' has been renamed to '$NEW_BRANCH'." echo "- You are now on branch '$(git_current_branch)'" echo } # # Assertions for use in git-flow subcommands # require_git_repo() { git rev-parse 2>/dev/null || die "Not a git repository" } require_gitflow_initialized() { gitflow_is_initialized || die "Not a gitflow-enabled repo yet. Please run 'git flow init' first." $(git config --get gitflow.prefix.versiontag >/dev/null 2>&1) || die "Version tag not set. Please run 'git flow init'." } require_clean_working_tree() { local result git_is_clean_working_tree result=$? if [ $result -eq 1 ]; then die "Working tree contains unstaged changes. Aborting." fi if [ $result -eq 2 ]; then die "Index contains uncommited changes. Aborting." fi } require_base_is_local_branch() { git_local_branch_exists "$1" || die "Base '$1' needs to be a branch. It does not exist and is required." } require_local_branch() { git_local_branch_exists "$1" || die "Local branch '$1' does not exist and is required." } require_remote_branch() { git_remote_branch_exists "$1" || die "Remote branch '$1' does not exist and is required." } require_branch() { git_branch_exists "$1" || die "Branch '$1' does not exist and is required." } require_branch_absent() { git_branch_exists "$1" && die "Branch '$1' already exists. Pick another name." } require_local_branch_absent() { git_local_branch_exists "$1" && die "Branch '$1' already exists. Pick another name." } require_tag_absent() { git_tag_exists "$1" && die "Tag '$1' already exists. Pick another name." } require_branches_equal() { local compare_refs_result require_local_branch "$1" require_remote_branch "$2" git_compare_refs "$1" "$2" compare_refs_result=$? if [ $compare_refs_result -gt 0 ]; then warn "Branches '$1' and '$2' have diverged." if [ $compare_refs_result -eq 1 ]; then die "And branch '$1' may be fast-forwarded." elif [ $compare_refs_result -eq 2 ]; then # Warn here, since there is no harm in being ahead warn "And local branch '$1' is ahead of '$2'." else die "Branches need merging first." fi fi } # # Show commands if flag is set. # git_do() { if flag showcommands; then echo "git $@" >&2 fi git "$@" } # # run_filter_hook # # Looks for a Git hook script called as defined by the first variable # # filter-flow-command # # If such a hook script exists and is executable, it is called with the given # positional arguments. # run_filter_hook() { local command scriptfile return command=$1 shift scriptfile="${HOOKS_DIR}/filter-flow-${command}" if [ -x "$scriptfile" ]; then return=`$scriptfile "$@"` if [ $? -eq 127 ]; then echo "$return" exit 127 fi echo $return else echo "$@" fi } # # run_pre_hook # # Looks for a Git hook script called # # pre-flow-- # # If such a hook script exists and is executable, it is called with the given # positional arguments. If its return code non-zero, the git-flow action is # aborted. # run_pre_hook() { local scriptfile exitcode scriptfile="${HOOKS_DIR}/pre-flow-${SUBCOMMAND}-${SUBACTION}" exitcode=0 if [ -x "$scriptfile" ]; then "$scriptfile" "$@" exitcode=$? if [ $exitcode -gt 0 ]; then die "Hook command $scriptfile ended with exit code $exitcode." fi fi } # # run_post_hook # # Looks for a Git hook script called # # post-flow-- # # If such a hook script exists and is executable, it is called with the given # positional arguments. Its return code is ignored. # run_post_hook() { local scriptfile scriptfile="${HOOKS_DIR}/post-flow-${SUBCOMMAND}-${SUBACTION}" if [ -x "$scriptfile" ]; then "$scriptfile" "$@" fi } flags_help() { eval "$( echo "$OPTIONS_SPEC" | git rev-parse --parseopt -- "-h" || echo exit $? )" }