0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00
Anki-Android/tools/gitflow-integrate
Flavio Lerda 287af3fc3d Move gitflow-integrate into tools directory.
I mistankely put this tool in the root directory. Also, it lost its
executable bit, so restoring it.
2014-09-03 05:42:04 +01:00

407 lines
12 KiB
Bash
Executable File

#!/bin/bash
#
# This script is meant to be used to integrate changes from a release or hotfix
# branch into the develop branch.
#
# Use:
# gitflow-integrate
# to see how to use the tool.
# Add this directory to the path.
PATH=$(dirname $0)/lib:$PATH
. bash-util.sh || exit 1
. git-util.sh || exit 1
# Executes the given command, discarding any output to stderr.
#
# arguments:
# the command to execute
#
# stdout:
# the output of the command
#
# return:
# the return value of the command
function quietly() {
"$@" 2>/dev/null
}
# Executes the given command, discarding any output to stdout or stderr.
#
# arguments:
# the command to execute
#
# return:
# the return value of the command
function silently() {
"$@" 2>/dev/null >/dev/null
}
# Outputs an error message and exits the script.
#
# arguments:
# message: one or more strings, the message to be printed
#
# stderr:
# the message
function fatal() {
echo "FATAL: $*" 1>&2
exit 113 # Use a special error code so that we can identify fatal failures.
}
# Output an error message for a missing argument and terminates with a fatal
# error.
#
# arguments:
# name: the name of the missing argument
#
# stderr:
# an error message for the missing argument (to stderr)
function missing_arg() {
fatal "Missing argument: $1"
}
# Output an error message if given any arguments and (in that case) also
# terminates with a fatal error.
#
# arguments:
# arguments: the remaining arguments to the function
function no_more_args() {
test $# = 0 || fatal "Too many arguments: $*"
}
# Outputs the body of the commit message for a given commit.
#
# arguments:
# commit: the SHA of the commit
#
# stdout:
# the body of the commit message
function git_commit_body() {
local commit="$1"; shift || missing_arg commit
no_more_args "$@"
git log -n1 --format="%B" "$commit" | cat
}
# Outputs the SHAs of the commits up to the given one that are not yet
# integrated into the base commit.
#
# arguments:
# commit: the SHA of the commit to integrate
# base: the SHA of the base commit to integrate into
#
# Outputs:
# the abbreviated SHAs of all commits that still need to be integrated
function git_commits_not_in() {
local commit="$1"; shift || missing_arg commit
local base="$1"; shift || missing_arg base
no_more_args "$@"
git log --format='%h' --first-parent $base..$commit
}
# Outputs the abbreviated SHA for the given commit.
#
# arguments:
# commit: the SHA of a commit
#
# stdout:
# the abbreviated SHA of the commit
function git_abbrev() {
local commit="$1"; shift
test $# = 0
git log -n1 --format=%h "$commit"
}
# Commits the current merge using a specified message.
#
# arguments:
# passed to 'git commit' itself
#
# stdin:
# used as the commit message
function git_commit_merge_with_message() {
# Copy the message to the merge message file.
cat - >"$(git rev-parse --git-dir)/MERGE_MSG"
# And commit the changes without invoking the editor.
EDITOR=true git commit "$@"
}
function git_is_branch() {
local branch="$1"; shift || missing_arg branch
no_more_args "$@"
git for-each-ref --format='%(refname:short)' refs/heads/ |
silently grep "^$branch\$"
}
# Prints the message that should be used to integrate a given commit.
#
# The message can have an optional prefix put in front of it.
#
# If the commit is a merge, all changes that were not included in the base
# commit are added to the message.
#
# arguments:
# prefix: a prefix to be added to the message
# commit: the SHA of the commit to be integrated
# base: the SHA of the base commit into which to integrate
#
# stdout:
# the message to used to integrate commit into base
function message() {
local prefix="$1"; shift || missing_arg prefix
local commit="$1"; shift || missing_arg commit
local base="$1"; shift || missing_arg base
no_more_args "$@"
# Determines if this is a merge that we want to follow.
if git_commit_body "$commit" | silently grep "^Merge pull request #"; then
# This is merging a pull request: get the message from the second parent.
message "$prefix" "$commit^2" "$base"
elif git_commit_body "$commit" | silently grep "^Merge commit '"; then
# This is merging some other commit: get the message from the second parent.
message "$prefix" "$commit^2" "$base"
elif git_commit_body "$commit" | silently grep "^Merge branch '"; then
# This is merging some other branch: get the message from the second parent.
message "$prefix" "$commit^2" "$base"
else
if [ $(git_commits_not_in "$commit" "$base" | wc -l) = 1 ]; then
# If there is only one commit to integrate, get its message body.
echo -n "$prefix"
git_commit_body "$commit"
else
# Otherwise we are actually integrating more than one commit here.
# Find all the commits being integrated in reverse order.
integrated_commits="$(git_commits_not_in "$commit" "$base" | tac)"
# Construct the subject line using those as a comma-separated list.
echo "$prefix${integrated_commits//$'\n'/,}"
echo # Add an empty line separating the body.
# Build the body using the commit messages of the integrated commits
# (still in reverse order).
for integrated_commit in $integrated_commits; do
echo "Commit '$integrated_commit'"
git_commit_body "$integrated_commit"
done
fi
fi
}
# Returns true if the message associated with integrating a commit contains a
# given string.
#
# arguments:
# string: to be looked for
# commit: the SHA of the commit to be integrated
# base: the SHA of the base commit into which to do the integrate
#
# return:
# true if the string was contained in the message
function message_contains() {
local string="$1"; shift || missing_arg string
local commit="$1"; shift || missing_arg commit
local base="$1"; shift || missing_arg base
no_more_args "$@"
message "" "$commit" "$base" | silently grep "$string"
}
# Determine whether a particular commit should be skipped when being integrated
# in the given base.
#
# arguments:
# commit: the SHA of the commit to be integrated
# base: the SHA into which the commit is to be integrated
# force_skip: set to 1 if the commit should be skipped
# force_merge: set to 1 if the commit should be merged
#
# return:
# zero if it should be skipped, non-zero otherwise
function should_skip() {
local commit="$1"; shift || missing_arg commit
local base="$1"; shift || missing_arg base
local force_skip="$1"; shift || missing_arg force_skip
local force_merge="$1"; shift || missing_arg force_merge
no_more_args "$@"
if [ "$force_skip" = 1 ]; then
# Must skip.
return 0
elif [ "$force_merge" = 1 ]; then
# Must merge.
return 1
elif message_contains "^@branch-specific$" "$commit" "$base"; then
# Skip anything marged with the @branch-specific tag.
return 0
else
# No reason to skip, then merge.
return 1
fi
}
# Integrates a single change into the given branch.
#
# It automatically determines if the commit should be skipped.
#
# If either force_skip or force_merge are set to 1, then the logic to
# automatically determine if the commit should be skipped or merged is ignored.
#
# arguments:
# commit: the SHA of the commit to be integrated
# from_branch: the name of the branch the commit comes from
# branch: the name of the branch into which the commit should be integrated
# force_skip: set to 1 if the commit should be skipped
# force_merge: set to 1 if the commit should be merged
function integrate() {
local commit="$1"; shift || missing_arg commit
local from_branch="$1"; shift || missing_arg from_branch
local branch="$1"; shift || missing_arg branch
local force_skip="$1"; shift || missing_arg force_skip
local force_merge="$1"; shift || missing_arg force_merge
no_more_args "$@"
echo "Merging '$commit'"
local base="$(git_abbrev "$branch")"
if should_skip "$commit" "$base" "$force_skip" "$force_merge"; then
# We should skip this change: we create a merge commit, but ignore any
# change that it would bring in, and make an empty commit for it.
git merge "$commit" --no-ff --no-commit -s ours
# Finalize the commit by specifying the right message.
local prefix="Skipped '$(git_abbrev "$commit")' from $from_branch: "
message "$prefix" "$commit" "$base" |
git_commit_merge_with_message --allow-empty
# Just to be sure, check that there are no changes in the newly commited
# change.
if [ $(git diff HEAD^ | wc -l) != 0 ]; then
echo "WARNING: Skipped change actually had changes."
fi
else
# Otherwise, we should merge the changes from this commit.
git merge "$commit" --no-ff --no-commit || (
# If there are any conflicts, ask the user to resolve them.
echo "Please resolve conflict and press ENTER"
read
)
local prefix="Merged '$(git_abbrev "$commit")' from $from_branch: "
message "$prefix" "$commit" "$base" |
git_commit_merge_with_message
fi
}
# Show the commit message that would be associated with integrating a commit
# into a branch.
#
# args:
# commit: the SHA of the commit to be integrated
# from_branch: the name of the branch the commit comes from
# branch: the name of the branch into which the commit should be integrated
# force_skip: set to 1 if the commit should be skipped
# force_merge: set to 1 if the commit should be merged
#
# stdout:
# the message to be associated with the specified commit being integrated
function show() {
local commit="$1"; shift || missing_arg commit
local from_branch="$1"; shift || missing_arg from_branch
local branch="$1"; shift || missing_arg branch
local force_skip="$1"; shift || missing_arg force_skip
local force_merge="$1"; shift || missing_arg force_merge
no_more_args "$@"
local base="$(git_abbrev "$branch")"
if should_skip "$commit" "$base" "$force_skip" "$force_merge"; then
# Finalize the commit by specifying the right message.
local prefix="Skipped '$(git_abbrev "$commit")' from $from_branch: "
message "$prefix" "$commit" "$base"
else
local prefix="Merged '$(git_abbrev "$commit")' from $from_branch: "
message "$prefix" "$commit" "$base"
fi
}
# Prints a help message for the possible commands.
function show_help() {
no_more_args "$@"
prog_name="$(basename $0)"
echo "usage:"
echo
echo " $prog_name apply <from> <into>"
echo " Integrates a single commit in <from> to <into>."
echo " Automatically determine whether to skip or merge."
echo
echo " $prog_name skip <from> <into>"
echo " Integrates by skipping a single commit in <from> to <into>."
echo
echo " $prog_name merge <from> <into>"
echo " Integrates by merging a single commit in <from> to <into>."
echo
echo " $prog_name show <from> <into>"
echo " Shows the message associated with integrating a single commit"
echo " in <from> to <into>."
}
# This is the main body of the script.
function main() {
if [ $# = 0 ]; then
no_more_args "$@"
show_help
return 0
fi
local command="$1"; shift || missing_arg command
local from="$1"; shift || missing_arg from
local into="$1"; shift || missing_arg into
no_more_args "$@"
git_is_branch "$from" || fatal "Invalid branch: $from"
git_is_branch "$into" || fatal "Invalid branch: $into"
# Find the first commit to be integrated, i.e., the oldest one.
first_commit=$(git_commits_not_in "$from" "$into" | tail -1)
if [ "$first_commit" = "" ]; then
echo "Nothing to integrate."
return 1
fi
if [ "$command" = "apply" ]; then
integrate "$first_commit" "$from" "$into" "0" "0"
elif [ "$command" = "skip" ]; then
integrate "$first_commit" "$from" "$into" "1" "0"
elif [ "$command" = "merge" ]; then
integrate "$first_commit" "$from" "$into" "0" "1"
elif [ "$command" = "show" ]; then
show "$first_commit" "$from" "$into" "0" "0"
else
echo "Unknown command: $command"
return 1
fi
}
# Fail immediate if any command fails.
set -e -o pipefail
main "$@"