Created
October 22, 2025 14:37
-
-
Save tayyebi/fcdf3fe4611c63c21446189bbc76a7de to your computer and use it in GitHub Desktop.
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
| #!/bin/bash | |
| # Usage: ./usr/local/bin/cherry-pick <second_branch> [author-or-username] [--commit] [--commit-separate] | |
| # Examples: | |
| # ./usr/local/bin/cherry-pick feature-branch | |
| # ./usr/local/bin/cherry-pick feature-branch "[email protected]" --commit | |
| # ./usr/local/bin/cherry-pick feature-branch --commit-separate | |
| # Description: | |
| # Find commits in <second_branch> newer than the last commit on the current | |
| # branch by the same author (or provided author/username). By default the script | |
| # stages the combined changes without creating commits. Use --commit to create | |
| # a single combined commit, or --commit-separate to replay each found commit | |
| # as an individual commit preserving original author and message. | |
| set -euo pipefail | |
| if [ -z "${1-}" ]; then | |
| echo "Usage: $0 <second_branch> [author-or-username] [--commit] [--commit-separate]" | |
| exit 1 | |
| fi | |
| SECOND_BRANCH="$1" | |
| ARG2="${2-}" | |
| COMMIT_FLAG=false | |
| COMMIT_SEPARATE=false | |
| # parse remaining args | |
| for arg in "${@:2}"; do | |
| case "$arg" in | |
| --commit) COMMIT_FLAG=true ;; | |
| --commit-separate) COMMIT_SEPARATE=true ;; | |
| *) AUTHOR_ARG="$arg" ;; | |
| esac | |
| done | |
| # ensure repo | |
| if ! git rev-parse --git-dir >/dev/null 2>&1; then | |
| echo "Error: not a git repository" | |
| exit 2 | |
| fi | |
| CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) | |
| LAST_COMMIT_DATE=$(git log -1 --format="%ct" "$CURRENT_BRANCH" 2>/dev/null || echo 0) | |
| # Determine author identifier (prefer provided arg). Use username (email local-part) from last commit when not provided. | |
| if [ -n "${AUTHOR_ARG-}" ]; then | |
| AUTHOR_IDENTIFIER="$AUTHOR_ARG" | |
| else | |
| LAST_AUTHOR_EMAIL=$(git log -1 --format="%ae" "$CURRENT_BRANCH" 2>/dev/null || echo "") | |
| if [ -n "$LAST_AUTHOR_EMAIL" ]; then | |
| AUTHOR_IDENTIFIER="${LAST_AUTHOR_EMAIL%%@*}" # username (local-part) | |
| else | |
| AUTHOR_IDENTIFIER=$(git log -1 --format="%an" "$CURRENT_BRANCH" 2>/dev/null || echo "") | |
| fi | |
| fi | |
| echo "Current branch: $CURRENT_BRANCH" | |
| echo "Second branch: $SECOND_BRANCH" | |
| echo "Author identifier (username/email/regex): $AUTHOR_IDENTIFIER" | |
| echo "Last commit date (epoch): $LAST_COMMIT_DATE" | |
| echo "Create combined commit: $COMMIT_FLAG" | |
| echo "Replay as separate commits: $COMMIT_SEPARATE" | |
| # check branch exists | |
| if ! git show-ref --verify --quiet "refs/heads/$SECOND_BRANCH" && ! git rev-parse --verify --quiet "$SECOND_BRANCH" >/dev/null 2>&1; then | |
| echo "Error: branch '$SECOND_BRANCH' not found" | |
| exit 3 | |
| fi | |
| # collect qualifying commits (oldest->newest) | |
| mapfile -t NEW_COMMITS < <(git log --reverse "$SECOND_BRANCH" --author="$AUTHOR_IDENTIFIER" --format="%H %ct" | awk -v ref="$LAST_COMMIT_DATE" '$2 > ref {print $1}') | |
| if [ "${#NEW_COMMITS[@]}" -eq 0 ]; then | |
| echo "No newer commits matching '$AUTHOR_IDENTIFIER' found in '$SECOND_BRANCH'." | |
| exit 0 | |
| fi | |
| echo "Found ${#NEW_COMMITS[@]} commit(s) to apply." | |
| ORIG_HEAD=$(git rev-parse --verify HEAD 2>/dev/null || echo "none") | |
| # Helper: list unmerged files | |
| unmerged_files() { | |
| git diff --name-only --diff-filter=U || true | |
| } | |
| # Iterate commits | |
| for COMMIT in "${NEW_COMMITS[@]}"; do | |
| echo "Processing commit: $COMMIT" | |
| PREV_HEAD=$(git rev-parse --verify HEAD 2>/dev/null || echo "none") | |
| if [ "$COMMIT_SEPARATE" = true ]; then | |
| # Replay as individual commit: do normal cherry-pick (creates commit unless conflicts) | |
| if git cherry-pick --allow-empty "$COMMIT"; then | |
| echo "Cherry-picked $COMMIT as a separate commit without conflicts." | |
| continue | |
| fi | |
| echo "Conflict detected while cherry-picking $COMMIT; accepting theirs for conflicts." | |
| # Resolve conflicts by taking theirs, stage and continue | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| echo "Resolving conflict by taking theirs for: $file" | |
| git checkout --theirs -- "$file" || true | |
| git add -- "$file" | |
| done < <(unmerged_files) | |
| # If CHERRY_PICK_HEAD exists, continue; otherwise abort and restore | |
| GIT_DIR=$(git rev-parse --git-dir) | |
| if [ -e "$GIT_DIR/CHERRY_PICK_HEAD" ]; then | |
| if git cherry-pick --continue; then | |
| echo "cherry-pick --continue succeeded for $COMMIT." | |
| continue | |
| else | |
| echo "Error: failed to continue cherry-pick for $COMMIT. Aborting and restoring." | |
| git cherry-pick --abort || true | |
| if [ "$PREV_HEAD" != "none" ]; then git reset --hard "$PREV_HEAD" || true; fi | |
| exit 4 | |
| fi | |
| else | |
| echo "Warning: no cherry-pick in progress after resolving conflicts for $COMMIT." | |
| git add -A || true | |
| if git diff --name-only --diff-filter=U | read -r _; then | |
| echo "Remaining unmerged paths found. Restoring previous HEAD ($PREV_HEAD)." | |
| git reset --hard "$PREV_HEAD" || true | |
| exit 5 | |
| fi | |
| # create a commit using original commit metadata | |
| AUTHOR_LINE=$(git show -s --format='%an <%ae>' "$COMMIT") | |
| COMMIT_MSG=$(git show -s --format='%B' "$COMMIT") | |
| COMMIT_DATE=$(git show -s --format='%aI' "$COMMIT") | |
| GIT_COMMITTER_DATE="$COMMIT_DATE" git commit --author="$AUTHOR_LINE" -m "$COMMIT_MSG" | |
| echo "Committed resolved result for $COMMIT preserving original metadata." | |
| fi | |
| else | |
| # Default behavior: apply with --no-commit to stage changes (single combined commit later) | |
| if git cherry-pick --no-commit --allow-empty "$COMMIT"; then | |
| echo "Applied $COMMIT without conflicts (changes staged)." | |
| continue | |
| fi | |
| echo "Conflict detected while applying $COMMIT. Accepting incoming changes (theirs) for all conflicts." | |
| while IFS= read -r file; do | |
| [ -z "$file" ] && continue | |
| echo "Resolving conflict by taking theirs for: $file" | |
| git checkout --theirs -- "$file" || true | |
| git add -- "$file" | |
| done < <(unmerged_files) | |
| GIT_DIR=$(git rev-parse --git-dir) | |
| if [ -e "$GIT_DIR/CHERRY_PICK_HEAD" ]; then | |
| if git cherry-pick --continue; then | |
| # cherry-pick --continue created a commit; we reset soft to PREV_HEAD to leave changes staged as intended | |
| if [ "$PREV_HEAD" = "none" ]; then | |
| git reset --soft HEAD^ || true | |
| else | |
| git reset --soft "$PREV_HEAD" || true | |
| fi | |
| echo "Uncommitted the cherry-picked changes and left them staged." | |
| else | |
| echo "Error: failed to continue cherry-pick after resolving conflicts for $COMMIT." | |
| git cherry-pick --abort || true | |
| if [ "$PREV_HEAD" != "none" ]; then git reset --hard "$PREV_HEAD" || true; fi | |
| exit 4 | |
| fi | |
| else | |
| echo "Warning: no cherry-pick in progress after resolving conflicts for $COMMIT." | |
| git add -A || true | |
| if git diff --name-only --diff-filter=U | read -r _; then | |
| echo "Remaining unmerged paths found. Aborting and restoring previous HEAD ($PREV_HEAD)." | |
| git reset --hard "$PREV_HEAD" || true | |
| exit 5 | |
| fi | |
| fi | |
| fi | |
| done | |
| # After applying all commits: | |
| if [ "$COMMIT_SEPARATE" = true ]; then | |
| echo "All commits replayed as separate commits. Review the log to see new commits." | |
| exit 0 | |
| fi | |
| if [ "$COMMIT_FLAG" = true ]; then | |
| if git diff --staged --quiet; then | |
| echo "No staged changes to commit." | |
| else | |
| git commit -m "Cherry-picked ${#NEW_COMMITS[@]} commit(s) from $SECOND_BRANCH by $AUTHOR_IDENTIFIER" | |
| echo "Created combined commit." | |
| fi | |
| else | |
| echo "All cherry-picks applied and changes staged. No commit has been made." | |
| echo "To see these changes in your branch log, create a commit." | |
| echo "Inspect staged changes with: git diff --staged" | |
| echo 'Create a single commit with: git commit -m "<your combined message>"' | |
| echo "Or replay commits as separate commits by re-running with --commit-separate." | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment