Skip to content

Instantly share code, notes, and snippets.

@smola
Last active March 9, 2022 12:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smola/aafb2298cf86055e34e7bc89625920a7 to your computer and use it in GitHub Desktop.
Save smola/aafb2298cf86055e34e7bc89625920a7 to your computer and use it in GitHub Desktop.
git-cherry-list
#!/usr/bin/env bash
set -eu
PROG_NAME="$(basename -- "${BASH_SOURCE[0]}")"
print_usage() {
echo "Usage: $PROG_NAME [-i <ignored pattern>] <SOURCE> <TARGET>"
echo ""
echo "Examples:"
echo " # Compare master to branch v1.x"
echo " $PROG_NAME master v1.x"
echo
echo " # Compare master to branch v1.x, ignoring release commits"
echo " $PROG_NAME -i '[release]' master v1.x"
}
print_usage_and_exit() {
print_usage
exit 1
}
declare -a IGNORED_PATTERNS
while :; do
case "${1:-}" in
-i)
if [[ -z "${2:-}" ]]; then
print_usage_and_exit
fi
IGNORED_PATTERNS+=("${2}")
shift
shift
;;
*)
break
;;
esac
done
if [[ -z "${1:-}" || -z "${2:-}" || -n "${3:-}" ]]; then
print_usage_and_exit
fi
SOURCE="${1}"
TARGET="${2}"
array_contains() {
local needle="${1}"
shift
for el in "${@}"; do
if [[ "$needle" == "$el" ]]; then
return 0
fi
done
return 1
}
is_ignored() {
local commit="${1}"
[[ "${IGNORED[$commit]:-}" == true ]]
}
mapfile -t NOT_IN_TARGET < <(git log --pretty=%H "${SOURCE}" "^${TARGET}")
mapfile -t NOT_IN_SOURCE < <(git log --pretty=%H "${TARGET}" "^${SOURCE}")
COMMITS=("${NOT_IN_TARGET[@]}" "${NOT_IN_SOURCE[@]}")
declare -A SUBJECTS
declare -A BODIES
declare -A DIFFS
declare -A IGNORED
for commit in "${COMMITS[@]}"; do
SUBJECTS[$commit]="$(git log --format=%s -n 1 "${commit}")"
BODIES[$commit]="$(git log --format=%B -n 1 "${commit}")"
DIFFS[$commit]="$(git show --pretty=oneline --patch --unified=0 --no-indent-heuristic --minimal --ignore-all-space --ignore-blank-lines "${commit}" | tail -n +4)"
IGNORED[$commit]=
for ignore_pattern in "${IGNORED_PATTERNS[@]}"; do
if echo "${BODIES[$commit]}" | grep -q "${ignore_pattern}"; then
IGNORED[$commit]=true
break
fi
done
done
declare -A CHERRY_PICKED
for source_commit in "${NOT_IN_TARGET[@]}"; do
if is_ignored "${source_commit}"; then
continue
fi
for target_commit in "${NOT_IN_SOURCE[@]}"; do
if is_ignored "${target_commit}"; then
continue
fi
if [[ "${DIFFS[$source_commit]}" == "${DIFFS[$target_commit]}" ]]; then
CHERRY_PICKED[$source_commit]="$target_commit"
break
elif [[ "${BODIES[$target_commit]}" =~ cherry\ picked\ from\ commit\ ${source_commit} ]]; then
CHERRY_PICKED[$source_commit]="$target_commit"
break
elif [[ "${BODIES[$target_commit]}" == *"${SUBJECTS[$source_commit]}"* ]]; then
CHERRY_PICKED[$source_commit]="$target_commit"
break
fi
done
if [[ -n "${CHERRY_PICKED[$source_commit]:-}" ]]; then
echo -e "${source_commit}\t${CHERRY_PICKED[$source_commit]}"
else
echo "${source_commit}"
fi
done
for target_commit in "${NOT_IN_SOURCE[@]}"; do
if is_ignored "${target_commit}"; then
continue
fi
if ! array_contains "${target_commit}" "${CHERRY_PICKED[@]}"; then
echo -e "\t${target_commit}"
fi
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment