Last active
September 18, 2023 11:03
-
-
Save e111077/0c24fc9d84557ec594ab376ef991e147 to your computer and use it in GitHub Desktop.
Git evolve (untested from GPT4)
This file contains 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
#!/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