Last active
November 30, 2025 08:40
-
-
Save bsitruk/5301082b06c29c392c70e2a44086a067 to your computer and use it in GitHub Desktop.
Check git rename detection before merging main into a feature branch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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