Last active
September 3, 2025 11:51
-
-
Save mgerhardy/5a2d87f0de15f8cfc6932559f195f200 to your computer and use it in GitHub Desktop.
This script cherry-picks a commit and applies clang-format to the changed files while preserving the original commit metadata.
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 | |
| # git-cherry-pick-format - Git extension for cherry-picking and formatting commits | |
| # Usage: git cherry-pick-format [--verbose] [--help] <commit-id> | |
| # | |
| # This script cherry-picks a commit and applies clang-format to the changed files | |
| # while preserving the original commit metadata. | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| verbose=false | |
| # Function for verbose logging | |
| log() { | |
| if $verbose; then | |
| echo "$@" | |
| fi | |
| } | |
| # Function to display usage information | |
| show_usage() { | |
| echo "Usage: git cherry-pick-format [--verbose] [--help] <commit-id>" | |
| echo "" | |
| echo "Cherry-pick a commit and apply clang-format to the changed files." | |
| echo "Preserves original commit metadata and handles multiple .clang-format files." | |
| echo "" | |
| echo "Options:" | |
| echo " --verbose Enable detailed output" | |
| echo " --help Show this help message and exit" | |
| exit 1 | |
| } | |
| # Function to display error messages | |
| error_exit() { | |
| echo "Error: $1" >&2 | |
| exit 1 | |
| } | |
| # Cleanup function | |
| cleanup_temp_branch() { | |
| if git rev-parse --verify "$temp_branch" >/dev/null 2>&1; then | |
| git branch -D "$temp_branch" >/dev/null 2>&1 || true | |
| fi | |
| } | |
| # Check for clang-format | |
| check_clang_format() { | |
| if ! command -v clang-format >/dev/null 2>&1; then | |
| error_exit "clang-format is not installed or not in PATH" | |
| fi | |
| } | |
| # Copy .clang-format files | |
| copy_clang_format_files() { | |
| local current_branch="$1" | |
| shift | |
| local format_files=("$@") | |
| local copied_files=() | |
| for format_file in "${format_files[@]}"; do | |
| if [[ -n "$format_file" ]]; then | |
| if git show "$current_branch:$format_file" > "$format_file" 2>/dev/null; then | |
| copied_files+=("$format_file") | |
| log "Copied .clang-format file: $format_file" | |
| fi | |
| fi | |
| done | |
| printf '%s\n' "${copied_files[@]}" | |
| } | |
| # Remove copied .clang-format files | |
| remove_clang_format_files() { | |
| local format_files=("$@") | |
| for format_file in "${format_files[@]}"; do | |
| if [[ -n "$format_file" && -f "$format_file" ]]; then | |
| rm -f "$format_file" | |
| log "Removed copied .clang-format file: $format_file" | |
| fi | |
| done | |
| } | |
| # Apply clang-format to changed files | |
| format_changed_files() { | |
| local commit_hash="$1" | |
| local changed_files | |
| changed_files=$(git diff-tree --no-commit-id --name-only -r "$commit_hash" | \ | |
| grep -E '\.(c|cpp|cc|cxx|h|hpp|hxx|m|mm|hh|inl)$' || true) | |
| if [[ -z "$changed_files" ]]; then | |
| log "No C/C++ files found in commit $commit_hash" | |
| return 0 | |
| fi | |
| log "C/C++ files to format:" | |
| log "$changed_files" | |
| log "" | |
| log "Formatting changed C/C++ files..." | |
| local formatted_count=0 | |
| local total_files=0 | |
| while IFS= read -r file; do | |
| if [[ -n "$file" ]]; then | |
| total_files=$((total_files + 1)) | |
| if [[ -f "$file" ]]; then | |
| log "Formatting: $file" | |
| if clang-format -i "$file" 2>/dev/null; then | |
| formatted_count=$((formatted_count + 1)) | |
| log " ✓ Successfully formatted $file" | |
| else | |
| log " ✗ Failed to format $file" | |
| fi | |
| else | |
| log " ⚠ File not found: $file" | |
| fi | |
| fi | |
| done <<< "$changed_files" | |
| log "Formatting summary: $formatted_count/$total_files files successfully formatted" | |
| return 0 | |
| } | |
| # Parse options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --verbose) | |
| verbose=true | |
| shift | |
| ;; | |
| --help) | |
| show_usage | |
| ;; | |
| -*) | |
| echo "Unknown option: $1" | |
| show_usage | |
| ;; | |
| *) | |
| break | |
| ;; | |
| esac | |
| done | |
| # Check if we're in a git repository | |
| if ! git rev-parse --git-dir >/dev/null 2>&1; then | |
| error_exit "Not in a git repository" | |
| fi | |
| # Check arguments | |
| if [[ $# -ne 1 ]]; then | |
| show_usage | |
| fi | |
| commit_to_pick="$1" | |
| # Validate that the commit exists | |
| if ! git rev-parse --verify "$commit_to_pick" >/dev/null 2>&1; then | |
| error_exit "Invalid commit hash: $commit_to_pick" | |
| fi | |
| # Check if clang-format is available | |
| check_clang_format | |
| # Get the current HEAD branch | |
| current_branch=$(git rev-parse --abbrev-ref HEAD) | |
| if [[ "$current_branch" == "HEAD" ]]; then | |
| error_exit "You are in detached HEAD state. Please checkout a branch first." | |
| fi | |
| log "Current branch: $current_branch" | |
| log "Commit to cherry-pick: $commit_to_pick" | |
| # Get the parent commit of the commit to cherry-pick | |
| parent_commit=$(git rev-parse "${commit_to_pick}^") | |
| log "Parent commit: $parent_commit" | |
| # Create a unique temporary branch name | |
| temp_branch="temp-cherry-pick-format-$(date +%s)-$$" | |
| log "Creating temporary branch: $temp_branch" | |
| # Set up cleanup trap | |
| trap cleanup_temp_branch EXIT | |
| # Find all .clang-format files in the HEAD branch | |
| log "Finding .clang-format files in HEAD branch..." | |
| mapfile -t format_files < <(git ls-tree -r --name-only HEAD | grep -E '\.clang-format$' || true) | |
| # Create and checkout the temporary branch from the parent commit | |
| git checkout -b "$temp_branch" "$parent_commit" | |
| # Copy .clang-format files and format the parent commit state first | |
| log "Copying .clang-format files to temporary branch..." | |
| mapfile -t copied_format_files < <(copy_clang_format_files "${current_branch}" "${format_files[@]}") | |
| # Pre-format parent commit state | |
| changed_files_list=$(git diff-tree --no-commit-id --name-only -r "$commit_to_pick" | \ | |
| grep -E '\.(c|cpp|cc|cxx|h|hpp|hxx)$' || true) | |
| if [[ -n "$changed_files_list" ]]; then | |
| log "Files to pre-format in parent state:" | |
| log "$changed_files_list" | |
| log "" | |
| preformat_count=0 | |
| while IFS= read -r file; do | |
| if [[ -n "$file" && -f "$file" ]]; then | |
| log "Pre-formatting parent state: $file" | |
| if clang-format -i "$file" 2>/dev/null; then | |
| git add "$file" | |
| preformat_count=$((preformat_count + 1)) | |
| log " ✓ Pre-formatted $file" | |
| else | |
| log " ✗ Failed to pre-format $file" | |
| fi | |
| fi | |
| done <<< "$changed_files_list" | |
| if [[ $preformat_count -gt 0 ]]; then | |
| git commit -m "Pre-format parent state before applying changes from $commit_to_pick | |
| This commit applies clang-format to the parent commit state | |
| to create a properly formatted base for the patch." | |
| fi | |
| fi | |
| remove_clang_format_files "${copied_format_files[@]}" | |
| # Apply original commit | |
| if git -c merge.ours.driver=true cherry-pick --strategy=recursive -X ignore-space-change "$commit_to_pick"; then | |
| log "Successfully applied changes using merge strategy" | |
| elif git cherry-pick --strategy=resolve "$commit_to_pick"; then | |
| log "Successfully applied changes using resolve strategy" | |
| elif git cherry-pick -X ignore-all-space "$commit_to_pick"; then | |
| log "Successfully applied changes ignoring all space differences" | |
| else | |
| echo "All automatic strategies failed. Manual conflict resolution required." | |
| echo "After resolving conflicts, run: git cherry-pick --continue" | |
| exit 1 | |
| fi | |
| # Copy .clang-format files again and format the result | |
| log "Copying .clang-format files for post-formatting..." | |
| mapfile -t copied_format_files < <(copy_clang_format_files "${current_branch}" "${format_files[@]}") | |
| format_changed_files "$commit_to_pick" | |
| remove_clang_format_files "${copied_format_files[@]}" | |
| if ! git diff --quiet; then | |
| log "Committing post-cherry-pick formatting changes..." | |
| git diff-tree --no-commit-id --name-only -r "$commit_to_pick" | while IFS= read -r file; do | |
| if [[ -n "$file" && -f "$file" ]]; then | |
| git add "$file" | |
| fi | |
| done | |
| git commit --amend -C "$commit_to_pick" | |
| log "Applied formatting and preserved original commit metadata" | |
| else | |
| log "No additional formatting changes needed." | |
| git commit --amend -C "$commit_to_pick" | |
| log "Preserved original commit metadata" | |
| fi | |
| formatted_commit=$(git rev-parse HEAD) | |
| log "Formatted commit created: $formatted_commit" | |
| log "Switching back to $current_branch..." | |
| git checkout "$current_branch" | |
| if ! git cherry-pick "$formatted_commit"; then | |
| echo "Cherry-pick of formatted commit failed. Resolve conflicts manually." | |
| echo "Formatted commit is available at: $formatted_commit" | |
| exit 1 | |
| fi | |
| echo "Successfully cherry-picked and formatted commit: $commit_to_pick" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment