CardiacPhase/Git/usr/bin/gitflow-common

811 lines
19 KiB
Bash

# $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/<origin>/<branch>
#
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 <name>"
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 <version>"
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 <base>"
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 <name> 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 <version> 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-<subcmd>-<subaction>
#
# 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-<subcmd>-<subaction>
#
# 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 $? )"
}