Skip to content

Instantly share code, notes, and snippets.

@e111077
Last active September 18, 2023 11:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save e111077/0c24fc9d84557ec594ab376ef991e147 to your computer and use it in GitHub Desktop.
Save e111077/0c24fc9d84557ec594ab376ef991e147 to your computer and use it in GitHub Desktop.
Git evolve (untested from GPT4)
#!/bin/bash
# git-evolve:
# A custom Git command to rebase a sequence of branches in order over a base branch.
# It also supports the ability to continue after conflicts and check the current status of the evolution.
#
# Installation Instructions for zsh env:
# 1. Save this script to a location, e.g., ~/bin/git-evolve.
# 2. Make sure the script is executable: chmod +x ~/bin/git-evolve.
# 3. Add the following line to your .zshrc or .zshenv: export PATH="$HOME/bin:$PATH".
# 4. Restart your terminal or run 'source .zshrc' or 'source .zshenv'.
# 5. You can now use this script as 'git evolve'.
if [ -z "$1" ]; then
echo "Error: No arguments provided. Use 'git evolve --help' for usage information."
exit 1
fi
# Display help information
if [ "$1" == "--help" ]; then
echo "git evolve - Rebase a sequence of branches in order over a base branch."
echo ""
echo "Usage:"
echo " git evolve <baseBranch> <branch1> [<branch2> ...] Rebase the branches in sequence over the base branch."
echo " git evolve --continue Continue the rebasing process after resolving conflicts."
echo " git evolve --status Display the current status of the evolution."
echo " git evolve --abort Abort the current evolution process and reset branches to their original state."
echo " git evolve --help Display this help information."
echo ""
echo "Example:"
echo " git evolve master feature1 feature2"
echo " This will rebase feature1 over master, then feature2 over feature1."
echo ""
exit 0
fi
# Check if branch exists (both locally and remotely)
check_branch_exists() {
if ! git show-ref --verify --quiet refs/heads/$1 && ! git show-ref --verify --quiet refs/remotes/origin/$1; then
echo "Error: Branch '$1' does not exist locally or remotely."
non_existent_branches+=("$1")
fi
}
# Before starting the evolution, check if all branches exist and remember the original branch
non_existent_branches=()
for branch in "$@"; do
if [[ "$branch" != "--continue" && "$branch" != "--status" && "$branch" != "--help" && "$branch" != "--abort" ]]; then
check_branch_exists $branch
fi
done
if [[ ${#non_existent_branches[@]} -gt 0 ]]; then
echo "The following branches do not exist:"
for branch in "${non_existent_branches[@]}"; do
echo "- $branch"
done
exit 1
fi
# Get the root directory of the current Git repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
# Create a hash of the repo root for unique identification
if command -v sha256sum > /dev/null; then
REPO_HASH=$(echo $REPO_ROOT | sha256sum | cut -d ' ' -f1)
elif command -v shasum > /dev/null; then
REPO_HASH=$(echo $REPO_ROOT | shasum -a 256 | cut -d ' ' -f1)
else
echo "Error: Unable to find a compatible hashing tool."
exit 1
fi
# Directory to store each branch's original commit for --abort
ORIGINAL_COMMIT_DIR="$STATE_DIR/original_commits"
[[ ! -d "$ORIGINAL_COMMIT_DIR" ]] && mkdir -p "$ORIGINAL_COMMIT_DIR"
# Directory to store state
STATE_DIR="$HOME/bin/.git-evolve/$REPO_HASH"
[[ ! -d "$STATE_DIR" ]] && mkdir -p "$STATE_DIR"
if [ "$1" != "--continue" ]; then
originalBranch=$(git rev-parse --abbrev-ref HEAD)
echo $originalBranch > $STATE_DIR/original_branch
fi
# Function to clear saved state
clear_state() {
rm -rf $STATE_DIR
}
# Function to handle conflicts
handle_conflict() {
echo "Conflict detected while rebasing $1 onto $2."
# Save the current state
echo $1 > $STATE_DIR/current_branch
echo $2 > $STATE_DIR/previous_branch
shift 2 # Remove the current and previous branch from the arguments
echo "$@" > $STATE_DIR/remaining_branches
echo "Please fix the conflicts and after resolving them, run:"
echo "git evolve --continue"
exit 1
}
# Handle aborting the evolution
if [ "$1" == "--abort" ]; then
if [[ ! -d "$STATE_DIR" ]]; then
echo "No git evolution in progress."
exit 0
fi
for branch_file in $ORIGINAL_COMMIT_DIR/*; do
branch_name=$(basename "$branch_file")
original_commit=$(cat "$branch_file")
git branch -f "$branch_name" "$original_commit"
done
clear_state
echo "Git evolution aborted and branches reset to their original state."
exit 0
fi
# Display the current evolution status
if [ "$1" == "--status" ]; then
if [[ ! -f $STATE_DIR/current_branch || ! -f $STATE_DIR/previous_branch ]]; then
echo "No git evolution in progress."
exit 0
fi
if [ -d ".git/rebase-apply" ] || [ -d ".git/rebase-merge" ]; then
if [[ ! -f $STATE_DIR/current_branch ]]; then
echo "Error: State files missing. Cannot proceed."
exit 1
fi
current=$(cat $STATE_DIR/current_branch)
previous=$(cat $STATE_DIR/previous_branch)
echo "Need to resolve conflicts to rebase $current over $previous. Once resolved, run 'git evolve --continue'."
else
if [[ -f $STATE_DIR/current_branch && -f $STATE_DIR/previous_branch ]]; then
if [[ ! -f $STATE_DIR/current_branch ]]; then
echo "Error: State files missing. Cannot proceed."
exit 1
fi
current=$(cat $STATE_DIR/current_branch)
previous=$(cat $STATE_DIR/previous_branch)
remaining=($(cat $STATE_DIR/remaining_branches))
if [[ ${#remaining[@]} -gt 0 ]]; then
echo "Rebase completed for $current over $previous. Next, run 'git evolve --continue' to rebase the remaining branches: ${remaining[*]}"
else
echo "Rebase completed for $current over $previous. No more branches left in the evolution sequence."
fi
else
echo "No git evolution in progress."
fi
fi
exit 0
fi
# Handle continuation after resolving conflicts
if [ "$1" == "--continue" ]; then
if [ ! -f ".git/REBASE_HEAD" ] && [ ! -d ".git/rebase-apply" ] && [ ! -d ".git/rebase-merge" ]; then
echo "No conflicts to continue from. Perhaps you forgot to resolve some?"
exit 1
fi
# Continue the rebase process
git rebase --continue
if [ $? -ne 0 ]; then
# If there's still a conflict after running `git rebase --continue`
if [[ ! -f $STATE_DIR/current_branch ]]; then
echo "Error: State files missing. Cannot proceed."
exit 1
fi
current=$(cat $STATE_DIR/current_branch)
previous=$(cat $STATE_DIR/previous_branch)
echo "Conflict while rebasing $current over $previous. Resolve the conflicts and then run 'git evolve --continue'."
exit 1
fi
# If the rebase was successful, update the branches to be rebased
if [[ ! -f $STATE_DIR/current_branch ]]; then
echo "Error: State files missing. Cannot proceed."
exit 1
fi
current=$(cat $STATE_DIR/current_branch)
previous=$(cat $STATE_DIR/previous_branch)
remaining=($(cat $STATE_DIR/remaining_branches))
# Remove the current branch from the list of remaining branches
remaining=(${remaining[@]/$current})
if [[ ${#remaining[@]} -eq 0 ]]; then
clear_state
echo "Evolution completed successfully!"
exit 0
fi
# Recurse with the remaining branches
git evolve $previous "${remaining[@]}"
exit 0
fi
# Main rebase logic
baseBranch=$1
shift
previousBranch=$baseBranch
lastResolvedCommit=$baseBranch
for currentBranch in "$@"; do
originalCommitCurrent=$(git rev-parse $currentBranch)
echo $originalCommitCurrent > "$ORIGINAL_COMMIT_DIR/${currentBranch}"
# Capture the original commit ID of the previous branch
originalCommitPrevious=$(git rev-parse $previousBranch)
# Rebasing current branch over the previous branch using the --onto flag with original commit of previous branch
git checkout $currentBranch && git rebase --onto $lastResolvedCommit $originalCommitPrevious $currentBranch
# Check if there's a conflict
if [ $? -ne 0 ]; then
handle_conflict $currentBranch $previousBranch "$@"
exit 1
fi
# Update the last resolved commit to be the latest commit of the successfully rebased branch
lastResolvedCommit=$(git rev-parse HEAD)
# The current branch becomes the previous branch for the next iteration
previousBranch=$currentBranch
shift
done
# Return to original branch
originalBranch=$(cat $STATE_DIR/original_branch)
git checkout $originalBranch
clear_state
echo "Evolution completed successfully!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment