Skip to content

Instantly share code, notes, and snippets.

@mgerhardy
Last active September 3, 2025 11:51
Show Gist options
  • Select an option

  • Save mgerhardy/5a2d87f0de15f8cfc6932559f195f200 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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