Skip to content

Instantly share code, notes, and snippets.

@bsitruk
Last active November 30, 2025 08:40
Show Gist options
  • Select an option

  • Save bsitruk/5301082b06c29c392c70e2a44086a067 to your computer and use it in GitHub Desktop.

Select an option

Save bsitruk/5301082b06c29c392c70e2a44086a067 to your computer and use it in GitHub Desktop.
Check git rename detection before merging main into a feature branch
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $(basename "$0") <feature-branch> <rename-threshold-0-100>" >&2
exit 1
fi
FEATURE_BRANCH=$1
THRESHOLD=$2
MAIN_BRANCH=${MAIN_BRANCH:-main}
if ! [[ $THRESHOLD =~ ^[0-9]+$ ]] || (( THRESHOLD < 0 || THRESHOLD > 100 )); then
echo "Rename threshold must be an integer between 0 and 100." >&2
exit 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "This script must be run inside a Git repository." >&2
exit 1
fi
resolve_ref() {
local ref=$1
if git rev-parse --verify --quiet "$ref" >/dev/null 2>&1; then
printf '%s\n' "$ref"
elif git rev-parse --verify --quiet "origin/$ref" >/dev/null 2>&1; then
printf 'origin/%s\n' "$ref"
else
return 1
fi
}
MAIN_REF=$(resolve_ref "$MAIN_BRANCH") || {
echo "Cannot find branch or ref '$MAIN_BRANCH'." >&2
exit 1
}
FEATURE_REF=$(resolve_ref "$FEATURE_BRANCH") || {
echo "Cannot find branch or ref '$FEATURE_BRANCH'." >&2
exit 1
}
MERGE_BASE=$(git merge-base "$MAIN_REF" "$FEATURE_REF") || {
echo "Unable to determine merge base between $MAIN_REF and $FEATURE_REF." >&2
exit 1
}
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
COLOR_RESET=$'\033[0m'
COLOR_FILE=$'\033[1;36m'
COLOR_FEATURE=$'\033[1;34m'
COLOR_MAIN=$'\033[1;32m'
else
COLOR_RESET=''
COLOR_FILE=''
COLOR_FEATURE=''
COLOR_MAIN=''
fi
collect_renames() {
local from_commit=$1
local to_commit=$2
git diff --find-renames="$THRESHOLD" --name-status "$from_commit" "$to_commit" |
while IFS=$'\t' read -r status from_path to_path; do
[[ $status == R* ]] || continue
local score=${status#R}
printf '%s\t%s\t%s\n' "$score" "$from_path" "$to_path"
done
}
collect_modifications() {
local from_commit=$1
local to_commit=$2
git diff --name-status "$from_commit" "$to_commit" |
while IFS=$'\t' read -r status path _; do
[[ $status == M* ]] || continue
printf '%s\n' "$path"
done
}
branch_renames=()
while IFS= read -r entry; do
[[ -z $entry ]] && continue
branch_renames+=("$entry")
done < <(collect_renames "$MERGE_BASE" "$FEATURE_REF")
main_renames=()
while IFS= read -r entry; do
[[ -z $entry ]] && continue
main_renames+=("$entry")
done < <(collect_renames "$MERGE_BASE" "$MAIN_REF")
branch_mods=()
while IFS= read -r path; do
[[ -z $path ]] && continue
branch_mods+=("$path")
done < <(collect_modifications "$MERGE_BASE" "$FEATURE_REF")
main_mods=()
while IFS= read -r path; do
[[ -z $path ]] && continue
main_mods+=("$path")
done < <(collect_modifications "$MERGE_BASE" "$MAIN_REF")
list_contains() {
local target=$1
shift
local item
for item in "$@"; do
[[ $item == "$target" ]] && return 0
done
return 1
}
colorize_path() {
local path=$1
if [[ -n $COLOR_FILE ]]; then
printf '%s%s%s' "$COLOR_FILE" "$path" "$COLOR_RESET"
else
printf '%s' "$path"
fi
}
colorize_branch() {
local branch=$1
if [[ -n $COLOR_FEATURE && $branch == "$FEATURE_REF" ]]; then
printf '%s%s%s' "$COLOR_FEATURE" "$branch" "$COLOR_RESET"
elif [[ -n $COLOR_MAIN && $branch == "$MAIN_REF" ]]; then
printf '%s%s%s' "$COLOR_MAIN" "$branch" "$COLOR_RESET"
else
printf '%s' "$branch"
fi
}
print_section() {
local title=$1
shift
echo "$title"
if (( $# == 0 )); then
echo " (none)"
return
fi
local entry
for entry in "$@"; do
IFS=$'\t' read -r score from_path to_path <<<"$entry"
printf ' %s -> %s (similarity %s%%)\n' "$from_path" "$to_path" "$score"
done
}
print_section "Renames/moves unique to ${FEATURE_REF}:" "${branch_renames[@]}"
if (( ${#main_renames[@]} )); then
print_section "Renames/moves unique to ${MAIN_REF}:" "${main_renames[@]}"
fi
potential_conflicts=()
if (( ${#main_renames[@]} )); then
for entry in "${branch_renames[@]}"; do
IFS=$'\t' read -r score from_path to_path <<<"$entry"
if list_contains "$from_path" "${main_mods[@]}"; then
potential_conflicts+=("feature|$from_path|$to_path|$MAIN_REF")
fi
done
fi
if (( ${#main_renames[@]} )); then
for entry in "${main_renames[@]}"; do
IFS=$'\t' read -r score from_path to_path <<<"$entry"
if list_contains "$from_path" "${branch_mods[@]}"; then
potential_conflicts+=("main|$from_path|$to_path|$FEATURE_REF")
fi
done
fi
echo "Potential rename/modify conflicts:"
if (( ${#potential_conflicts[@]} == 0 )); then
echo " (none)"
else
for conflict in "${potential_conflicts[@]}"; do
IFS='|' read -r side from_path to_path other_ref <<<"$conflict"
colored_from=$(colorize_path "$from_path")
colored_to=$(colorize_path "$to_path")
other_branch=$(colorize_branch "$other_ref")
if [[ $side == feature ]]; then
rename_branch=$(colorize_branch "$FEATURE_REF")
else
rename_branch=$(colorize_branch "$MAIN_REF")
fi
printf ' %s renamed to %s on %s; modified on %s\n' "$colored_from" "$colored_to" "$rename_branch" "$other_branch"
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment