Skip to content

Instantly share code, notes, and snippets.

@chtitux
Created April 4, 2019 15:29
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chtitux/f0def1bc40de579a68786284454005c3 to your computer and use it in GitHub Desktop.
Save chtitux/f0def1bc40de579a68786284454005c3 to your computer and use it in GitHub Desktop.
Merge master script
#!/bin/bash
# Example of the slack-usernames file:
# han.solo@gitlab.com @UABCDEF123
# Fail script if any command fails
set -e
GITLAB_HOST=${GITLAB_HOST:-gitlab.com}
GITLAB_UPSTREAM_NAMESPACE=${GITLAB_UPSTREAM_NAMESPACE:-mycorpcompany}
TOOLS_DIR=$(dirname "$0")
NEWLINE=$'\n'
function usage {
local script_name
script_name=$(basename "$0")
cat <<HELP
$script_name (options) (<remote> <branch>)
Check build completion and prepare fabric commands.
Arguments <remote> and <branch> are optional. If provided, they let merge a custom remote branch to master instead of official dev branch.
Typical usage:
$script_name
Example of hotfix merge:
$script_name fmenou security/cve-1234
Options:
-h: this help message
-c: specific Slack Channel to speak to
-d: branch name destination. Defaults to 'master'
-f: force the operation without asking confirmation
-j: launch from a build Job (implies -f force)
HELP
exit 0
}
function is_not_human {
git config --global --get user.name || git config --global user.name "${GITLAB_USER_EMAIL}"
git config --global --get user.email || git config --global user.email "${GITLAB_USER_EMAIL}"
}
function check_args {
SLACK_CHANNEL="#eu-releases"
to_shift=0
force=0
destination="master"
while getopts "c:d:fjh" option; do
case $option in
c) SLACK_CHANNEL=$OPTARG; to_shift=$((to_shift+2)) ;;
d) destination=$OPTARG; to_shift=$((to_shift+2)) ;;
f) force=1; to_shift=$((to_shift+1)) ;;
h) usage ;;
j) is_not_human; force=1; to_shift=$((to_shift+1)) ;;
esac
done
}
function main {
check_args "$@"
shift $to_shift
check_working_copy
check_tools_up_to_date
if [ $# = 2 ]; then
remote_fetch=$1
head=$1/$2
else
remote_fetch=$(remote_name "push")
head=$remote_fetch/dev
fi
remote_push=$(remote_name "push")
target_head="$remote_push/$destination"
git fetch "$remote_fetch"
git fetch "$remote_push"
check_current_master
check_git_username
project=$(basename "$(git rev-parse --show-toplevel)")
author=$(git config user.name)
changelog=$(prepare_changelog)
formatted_changelog=$(prepare_formatted_changelog)
committers=$(prepare_committers)
formatted_commiters=$(prepare_formatted_committers)
echo -e "$changelog"
echo -e "$committers"
if [ "$force" == "0" ]; then
echo -n -e "Do you want to merge and push? [y/N] "
read answer
else
answer="y"
fi
if [ "$answer" == "y" ]; then
merge
fi
}
function check_working_copy {
if [ "$(git ls-files -cdmsu | wc -l)" -ne 0 ]; then
echo "Working copy and index not clear, can't proceed"
git status -s
echo "Aborting.."
exit 1
fi
}
function check_tools_up_to_date {
if [ -d "$TOOLS_DIR/.git" ]; then
pushd "$TOOLS_DIR" > /dev/null
local tools_remote="$(remote_name "fetch")"
git fetch $tools_remote
if [ $(git diff $tools_remote/master | wc -l) -ne 0 ]; then
echo -e '\033[41m /!\' "Your tools are not up-to-date." '/!\ \033[0m'
echo "Please pull latest changes from $tools_remote"
echo -n -e "Do you still want to continue? [y/N] "
read answer
if [ "$answer" != "y" ]; then
exit 1
fi
fi
popd > /dev/null
fi
}
function check_git_username {
if [ -z "$(git config user.name)" ]; then
echo "Please set a git user name."
exit 1
fi
}
function upstream_remote {
local direction="($1)"
local gitlab_host="(${GITLAB_HOST}|gitlab.com)"
local main_repos="(${GITLAB_UPSTREAM_NAMESPACE}|myteam|myanotherteam)"
local remote
remote=$(git remote -v | grep -E "$gitlab_host.$main_repos" | grep "$direction" | head -n1)
if [ -z "$remote" ]; then
>&2 echo -e '\033[41m' "No upstream remote found within /${gitlab_host}:${main_repos}/" '\033[0m'
>&2 echo "Check/change remote url with 'git remote -v' or change the env variable GITLAB_HOST"
exit 1
fi
echo "$remote"
}
function remote_group {
local upstream
upstream=$(upstream_remote "$@")
upstream=${upstream#*https://}
upstream=${upstream#*git@}
upstream=${upstream#*:}
upstream=${upstream%/*}
upstream=${upstream#*/}
echo "$upstream"
}
function remote_name {
upstream_remote "$@" | cut -f 1
}
function check_current_master {
if [ -f ".git/refs/heads/$destination" ]; then
local unshared_master_commits
unshared_master_commits=$(git rev-list "${target_head}..${destination}" | wc -l)
if [ "$unshared_master_commits" -ne 0 ]; then
echo "local '$destination' is ahead of $target_head by $unshared_master_commits commits:"
git log --boundary --graph --oneline "${target_head}..${destination}"
echo "Aborting.."
exit 2
fi
fi
}
function prepare_changelog {
echo "CHANGELOG $project - $author"
git log --boundary --graph --oneline "${target_head}..$head"
}
function prepare_formatted_changelog {
local scm_url
scm_url="https://${GITLAB_HOST}/$(remote_group "push")/${project}/commit/"
local git_format
git_format="• <${scm_url}%h|%h> %s (%an)"
local graph
graph=$(git log --graph --boundary --pretty=format:"${git_format}" "${target_head}..$head")
local simplify_graph='s/\*[ |\/\\]*//g'
local indent='s/|[ \\\/]\{1,\}/ /g'
local replace_boundary='s/^ *o • <\(.*\)>/◦ <\1>/g'
local cleaned_graph="$(echo -e "$graph" | grep '[*o]' - | sed -e "$simplify_graph;$indent;$replace_boundary")"
echo "$cleaned_graph"
}
function prepare_committers {
echo "Includes changes from $(commiters_emails | comma_join)"
}
function prepare_formatted_committers {
echo "Includes changes from $(email2slack "$(commiters_emails)" | comma_join)" | slack_links
}
function commiters_emails {
local revision_range="$remote_push/$destination..$head"
git_authors "$revision_range"
}
function git_authors {
local revision_range="$1"
git log --pretty="format:%ae" --no-merges "$revision_range" | sort | uniq
}
function email2slack {
local text="$1"
# This is a bit of hackery, but actually makes sense. Pipes creates subshells, so the content
# of the "while read" block is executed in a different environment and the
# outer $text is never mutated. Manually creating a subshell (with the parenthesis)
# fixes this problem, cf https://serverfault.com/a/259342
cat "$TOOLS_DIR/slack-usernames" | (while read line; do
email=$(echo $line | cut -d ' ' -f1)
slack=$(echo $line | cut -d ' ' -f2)
text="${text//$email/$slack}"
done
echo -e "$text"
)
}
function slack_links {
xargs | sed -E "s/@([^[[:blank:],]+]*)/<@\1|\1>/g"
}
function comma_join {
xargs | sed 's/ /, /g'
}
function merge {
git checkout "$destination"
git reset --hard "${target_head}"
git merge --no-edit --commit --no-ff "$head"
# Back to previous branch
git checkout -
if [ -n "$(git diff "$head..$destination")" ]; then
echo "ERROR: git diff $head..$destination is not empty."
echo "Ensure that dev won't override code on '$destination' (maybe a bad executed cherry-pick)."
echo "Aborting.."
exit 3
else
git push "$remote_push" "$destination"
sha1=$(git rev-parse --short "$destination")
formatted_subject="Merging into $destination ($author) ${sha1}$NEWLINE$formatted_commiters"
echo -e "$formatted_changelog" | slack-speak -n "${project}" -c "${SLACK_CHANNEL}" -s "$formatted_subject"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment