如何自动化移除仅更新github-metrics.svg时间戳的Git提交?
批量清理Git仓库中仅更新时间戳的提交
问题背景
我有一个Git仓库,自己是唯一贡献者,没有派生仓库,可以自由使用变基和强制推送操作。仓库共有22503个线性提交,无合并提交,大部分提交仅修改github-metrics.svg文件,其中许多仅修改文件底部的<span>Last updated $(TIMESTAMP) with lowlighter/metrics@$(VERSION)</span>行。
我希望清理仓库,移除所有仅含该变更的提交;若提交同时包含其他变更则保留。手动使用git rebase --interactive标记fixup可行,但提交数量超22k无法手动完成,如何自动化?
试过一些类似问题的方案,但不符合需求:一是会删除时间戳行,二是无法修改历史提交且仅适用于本地仓库。
2024年1月5日更新
我编写了shell脚本并通过git filter-branch --tree-filter执行,运行约20分钟后仍存在仅含该时间戳变更的提交,请问问题出在哪里?
脚本内容:
#!/bin/bash -e # Define the metrics file name metrics_file="github-metrics.svg" # Function to validate the number of arguments validate_arguments() { if [ "$#" -ne 1 ]; then echo "Error: Incorrect number of arguments. Usage: $0 <commit>" exit 0 fi } # Function to check if the commit exists check_commit_existence() { git rev-parse --quiet --verify "$1^{commit}" >/dev/null || { echo "Error: Commit $1 does not exist"; exit 0; } } # Function to check if the parent commit exists check_commit_parent_existence() { git rev-parse --quiet --verify "$1^" >/dev/null || { echo "Error: Parent of commit $1 does not exist"; exit 0; } } # Function to check if the metrics file exists check_metrics_exists() { git cat-file -e "$1:$metrics_file" || { echo "Error: $metrics_file does not exist in commit $1"; exit 0; } } # Function to check for changes in metrics file check_metrics_changes() { short_stat=$(git diff --shortstat "$1^" "$1" "$metrics_file") if [ -z "$short_stat" ]; then echo "Error: No changes in $metrics_file" exit 0 fi } # Function to check for excessive changes check_excessive_changes() { git diff --shortstat "$1^" "$1" | grep -q '^ 1 file changed, 1 insertion(+), 1 deletion(-)' || { echo "Error: Too many changes in $1"; exit 0; } } # Function to check for last updated change check_last_updated_change() { git_diff_output=$(git diff "$1^" "$1" --unified=0 --word-diff | tail +6) search_string='<span>Last updated .+, .+ \(timezone [A-Za-z/]+\) with lowlighter/metrics@[0-9.]+</span>' if ! echo "$git_diff_output" | grep --quiet -E "$search_string"; then echo "Error: No change to Last updated" exit 0 fi } # Main function main() { validate_arguments "$@" check_commit_existence "$1" check_commit_parent_existence "$1" check_metrics_exists "$1" check_metrics_changes "$1" check_excessive_changes "$1" #check_last_updated_change "$1" # Get the metrics file from the parent commit git checkout "$1^" "$metrics_file" } # Call the main function with passed arguments main "$@"
执行命令:
git filter-branch --tree-filter '~/bin/lastupdated.sh $GIT_COMMIT' HEAD
问题分析与解决
原脚本的核心问题
tree-filter逻辑误解:tree-filter的作用是修改当前提交的文件树,再基于修改后的树生成新提交。你的脚本把父提交的github-metrics.svg检出到当前树中,只会让新提交内容和父提交一致,但不会删除提交本身——Git仍会生成一个内容重复的新提交,而非跳过该提交。- 退出码逻辑错误:脚本中检查函数用
exit 0退出,而git filter-branch会将0退出码视为操作成功,继续生成新提交;只有脚本以非0码退出时,才会跳过当前提交。 - 关键检查未启用:注释掉了
check_last_updated_change函数,导致只要变更符合文件数量条件,即使不是时间戳行也会被修改,反而可能引入错误。
正确实现方案
方案1:用git filter-repo(推荐,速度更快)
git filter-repo是Git官方推荐替代filter-branch的工具,处理大规模仓库效率提升明显,步骤如下:
- 安装
git filter-repo(可通过包管理器或官方源码安装) - 创建Python脚本
filter_trivial_commits.py:
#!/usr/bin/env python3 import re from gitfilterrepo import Commit, Filter class TrivialCommitFilter(Filter): def filter_commit(self, commit: Commit) -> Commit | None: # 检查提交是否只修改了目标文件 if len(commit.file_changes) != 1: return commit change = commit.file_changes[0] if change.filename != "github-metrics.svg": return commit # 检查变更是否仅为Last updated行 diff = change.diff.decode('utf-8') delete_match = re.search(r'^-.*<span>Last updated .+ with lowlighter/metrics@[0-9.]+\</span>', diff, re.MULTILINE) add_match = re.search(r'^\+.*<span>Last updated .+ with lowlighter/metrics@[0-9.]+\</span>', diff, re.MULTILINE) # 确认只有这两行变更 if delete_match and add_match and len([line for line in diff.splitlines() if line.startswith(('+', '-'))]) == 2: # 跳过该提交 return None return commit if __name__ == "__main__": TrivialCommitFilter().run()
- 执行过滤:
git filter-repo --commit-callback filter_trivial_commits.py
- 强制推送到远程:
git push --force origin HEAD
方案2:修复原filter-branch脚本
如果坚持使用filter-branch,修改脚本如下(改用--commit-filter直接控制提交是否保留):
#!/bin/bash -e metrics_file="github-metrics.svg" # 判断是否为仅修改时间戳的无用提交 is_trivial_commit() { local commit="$1" # 跳过初始提交 if ! git rev-parse --quiet --verify "$commit^" >/dev/null; then return 1 fi # 检查是否只修改了一个文件 local file_count=$(git diff --name-only "$commit^" "$commit" | wc -l) if [ "$file_count" -ne 1 ]; then return 1 fi # 检查是否是目标文件 local changed_file=$(git diff --name-only "$commit^" "$commit") if [ "$changed_file" != "$metrics_file" ]; then return 1 fi # 检查变更是否仅为Last updated行 local diff_lines=$(git diff "$commit^" "$commit" --unified=0 | grep -E '^[+-]') local delete_line=$(echo "$diff_lines" | grep '^-.*Last updated') local add_line=$(echo "$diff_lines" | grep '^+.*Last updated') [ -n "$delete_line" ] && [ -n "$add_line" ] && [ $(echo "$diff_lines" | wc -l) -eq 2 ] } main() { local commit="$1" if is_trivial_commit "$commit"; then # 非0码退出,让filter-branch跳过该提交 exit 1 fi # 保留提交,正常退出 exit 0 } main "$@"
执行命令:
git filter-branch --commit-filter '~/bin/lastupdated.sh $GIT_COMMIT || git commit-tree "$@"' HEAD
注意事项
- 操作前务必备份仓库:
git clone --mirror <仓库地址> backup-repo.git - 修改历史后推送需用
--force,确保无其他协作者使用该仓库(你是唯一贡献者,无风险) git filter-repo处理22k提交的速度远快于filter-branch,优先推荐使用
内容的提问来源于stack exchange,提问作者Amedee Van Gasse
相关产品推荐
相关产品推荐

