Skip to content

Instantly share code, notes, and snippets.

@convict-git
Last active March 12, 2023 01:50
Show Gist options
  • Save convict-git/8b2c33fe413f16ea1d4c9456b72d6889 to your computer and use it in GitHub Desktop.
Save convict-git/8b2c33fe413f16ea1d4c9456b72d6889 to your computer and use it in GitHub Desktop.
Rebase stacked branches easily
#!/bin/bash
set -euo pipefail
# Some constants
BASE_DIR="${HOME}/.rebase"
INDEX_LOG_FILE="index.log"
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
WHITE='\033[1;37m'
BOLD='\033[1m'
UNDERLINE='\033[4m'
NC='\033[0m' # No Color
# Logger function that logs messages with appropriate colors
function logger() {
local color=""
case "$1" in
"success") color="${GREEN}" ;;
"error") color="${RED}" ;;
"warn") color="${YELLOW}" ;;
"info") color="${WHITE}" ;;
*) color="${WHITE}" ;;
esac
printf "${color}$2${NC}\n"
}
# Run and log a command
function runAndLog {
local command="$1"
logger "info" "${command}"
eval "$command"
local status=$?
return $status
}
# Fetches branch, checks out the branch and rebases with origin/{branch}
function fetchCheckoutAndUpdate () {
local currentBranch=$1
local skipFetchAndUpdate=$2
if [ -z $skipFetchAndUpdate ] || [ $skipFetchAndUpdate -eq 0 ]; then
logger "info" "Fetching latest changes for ${BOLD}${UNDERLINE}$currentBranch${NC}..."
if ! runAndLog "git fetch origin ${currentBranch}"; then
logger "error" "Error fetching latest changes for ${BOLD}${UNDERLINE}$currentBranch${NC} from origin."
return 1
fi
fi
logger "info" "Checking out ${BOLD}${UNDERLINE}$currentBranch${NC}..."
if ! runAndLog "git checkout ${currentBranch}"; then
logger "error" "Error checking out ${BOLD}${UNDERLINE}$currentBranch${NC}."
return 1
fi
if [ -z $skipFetchAndUpdate ] || [ $skipFetchAndUpdate -eq 0 ]; then
logger "info" "Rebasing ${BOLD}${UNDERLINE}$currentBranch${NC} onto ${BOLD}${UNDERLINE}origin/$currentBranch${NC}..."
if ! runAndLog "git rebase origin/${currentBranch}"; then
logger "error" "Error rebasing ${BOLD}${UNDERLINE}$currentBranch${NC} onto ${BOLD}${UNDERLINE}origin/$currentBranch${NC}."
return 1
fi
fi
}
# Rebase for a given pair of branches and force pushes to origin
function rebasePair() {
local currentBranch=$1
local baseBranch=$2
local skipFetchAndUpdate=$3
if ! fetchCheckoutAndUpdate $currentBranch $skipFetchAndUpdate; then
return 1
fi
logger "info" "Rebasing ${BOLD}${UNDERLINE}$currentBranch${NC} onto ${BOLD}${UNDERLINE}$baseBranch${NC}..."
if ! runAndLog "git rebase ${baseBranch}"; then
logger "error" "Error rebasing ${BOLD}${UNDERLINE}$currentBranch${NC} onto ${BOLD}${UNDERLINE}$baseBranch${NC}."
return 1
fi
logger "info" "Pushing changes for ${BOLD}${UNDERLINE}$currentBranch${NC} to origin..."
if ! runAndLog "git push --force origin ${currentBranch}"; then
logger "error" "Error pushing changes for ${BOLD}${UNDERLINE}$currentBranch${NC} to origin."
return 1
fi
}
# Check that two command arguments were provided
if [ "$#" -ne 2 ]; then
logger "error" "Usage: $0 <git-repo-path> <branch-names-file>"
exit 1
fi
# Check that gitRepoPath is a valid directory
if [ ! -d "$1" ]; then
logger "error" "Error: '$1' is not a valid directory."
logger "error" "Usage: $0 <git-repo-path> <branch-names-file>"
exit 1
fi
# Check that branchNamesFile exists
if [ ! -f "$2" ]; then
logger "error" "Error: '$2' does not exist."
logger "error" "Usage: $0 <git-repo-path> <branch-names-file>"
exit 1
fi
# Take command arguments
gitRepoPath=$1
branchNamesFile=$2
startingDirectory=$(pwd)
function main() {
# Read the branch names from a plain text file given as second argument
local branches=($(cat $branchNamesFile))
local totalBranches=${#branches[@]}
# Change directory to gitRepoPath
runAndLog "cd ${gitRepoPath}"
# Make the BASE directory if it doesn't exist
mkdir -p $BASE_DIR
# Check if log file exists
local DEFAULT_STARTING_INDEX=1
local continuedPrevSession=0
if [ -f "${BASE_DIR}/${INDEX_LOG_FILE}" ]; then
# read the last index from log file
index=$(cat "${BASE_DIR}/${INDEX_LOG_FILE}")
if [[ ! $index =~ ^[0-9]+$ ]]; then
# the file contains an invalid index value, set the starting index to 0
index=$DEFAULT_STARTING_INDEX
else
logger "warn" "The last run stopped at ${BOLD}rebase of ${branches[$index]} onto ${branches[$index-1]}"
read -p "Do you want to continue from index $index? (y/n)" choice
if [ "$choice" != "y" ] && [ "$choice" != "Y" ]; then
index=$DEFAULT_STARTING_INDEX
fi
continuedPrevSession=1
fi
else
# set the starting index
index=$DEFAULT_STARTING_INDEX
fi
# Loop through the array of branch names and call the rebase function for each pair of branches
local encounteredError=0
for (( i=index; i<totalBranches; i++ ))
do
if [ $i -eq $DEFAULT_STARTING_INDEX ] && ! fetchCheckoutAndUpdate ${branches[$i-1]} $continuedPrevSession; then
logger "error" "${BOLD}Error during updating ${branches[$i-1]}. Aborting..."
break
fi
logger "warn" "\n=== Starting rebase of ${BOLD}${UNDERLINE}${branches[$i]}${NC} onto ${BOLD}${UNDERLINE}${branches[$i-1]}${NC}... ==="
if ! rebasePair "${branches[$i]}" "${branches[$i-1]}" $continuedPrevSession; then
logger "error" "${BOLD}Error during rebase of ${branches[$i]} onto ${branches[$i-1]}. Aborting..."
encounteredError=1
# Save the current index into log file
echo $i > $BASE_DIR/$INDEX_LOG_FILE
break
else
logger "success" "=== Completed rebase of ${BOLD}${UNDERLINE}${branches[$i]}${NC} onto ${BOLD}${UNDERLINE}${branches[$i-1]}${NC}... === \n\n"
fi
index=i
done
if [ $encounteredError -eq 1 ]; then
logger "error" "${BOLD}=== Some error occurred. If it is because of conflicts, do not worry, you will be able to continue from where we left! ===${NC}"
else
logger "success" "${BOLD}=== Completed rebasing!!! === \n\n${NC}"
# Remove the log file
rm -rf $BASE_DIR/$INDEX_LOG_FILE
fi
runAndLog "cd ${startingDirectory}"
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment