rt-thread/tools/ci/git_diff_show.py

372 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
# Copyright (c) 2025, RT-Thread Development Team
#
# SPDX-License-Identifier: Apache-2.0
#
# Change Logs:
# Date Author Notes
# 2025-05-15 supperthomas add PR status show
# 2025-05-22 hydevcode 替换bsp_building.yml的判断文件修改机制,并将PR status show合并进bsp_building.yml
import subprocess
import sys
import os
import re
import argparse
import locale
from typing import List, Dict
import json
from typing import List
class FileDiff:
def __init__(self, path: str, status: str, size_change: int = 0, old_size: int = 0, new_size: int = 0):
self.path = path
self.status = status # A (added), M (modified), D (deleted), R (renamed)
self.size_change = size_change
self.old_size = old_size
self.new_size = new_size
def __str__(self):
if self.status == 'A':
return f"Added {self.path}: {self.size_change} bytes"
elif self.status == 'D':
return f"Deleted {self.path}: was {self.old_size} bytes"
elif self.status == 'M' or self.status == 'R':
return f"Modified {self.path}: {self.size_change} bytes change"
else:
return f"{self.status} {self.path}"
class GitDiffAnalyzer:
def __init__(self, target_branch: str):
self.target_branch = target_branch
self.encoding = locale.getpreferredencoding()
def get_diff_files(self) -> List[FileDiff]:
"""获取当前分支与目标分支之间的差异文件"""
# 找到当前分支和目标分支的最近共同祖先
merge_base = self.get_merge_base()
if not merge_base:
print("No common ancestor found between current branch and target branch")
sys.exit(1)
# 获取差异文件列表
diff_cmd = f"git diff --name-status {merge_base} HEAD"
print(diff_cmd)
try:
output = subprocess.check_output(diff_cmd.split(), stderr=subprocess.STDOUT)
output = output.decode(self.encoding).strip()
print(output)
except subprocess.CalledProcessError as e:
print(f"Error executing git diff: {e.output.decode(self.encoding)}")
sys.exit(1)
if not output:
print("No differences between current branch and target branch")
sys.exit(0)
# 处理可能的换行符问题
output = output.replace('\r\n', '\n')
lines = output.split('\n')
file_diffs = []
for line in lines:
line = line.strip()
if not line:
continue
parts = line.split('\t')
if len(parts) < 2:
# 可能是重命名文件,格式为 "RXXX\told_path\tnew_path"
match = re.match(r'R(\d+)\t(.+)\t(.+)', line)
if match:
status = 'R'
old_path = match.group(2)
new_path = match.group(3)
# 计算重命名文件的修改大小
old_size = self.get_file_size(old_path, self.target_branch)
new_size = self.get_file_size(new_path, 'HEAD')
size_change = new_size - old_size if old_size > 0 else new_size
file_diffs.append(FileDiff(new_path, status, size_change, old_size, new_size))
else:
status = parts[0][0] # 取状态码的第一个字符
path = parts[1]
if status == 'A':
# 新增文件,计算大小
size = self.get_file_size(path, 'HEAD')
file_diffs.append(FileDiff(path, status, size, 0, size))
elif status == 'D':
# 删除文件,计算原大小
size = self.get_file_size(path, self.target_branch)
file_diffs.append(FileDiff(path, status, 0, size, 0))
elif status == 'M':
# 修改文件,计算大小变化
old_size = self.get_file_size(path, self.target_branch)
new_size = self.get_file_size(path, 'HEAD')
size_change = new_size - old_size
file_diffs.append(FileDiff(path, status, size_change, old_size, new_size))
return file_diffs
def get_merge_base(self) -> str:
"""获取当前分支和目标分支的最近共同祖先"""
try:
cmd = f"git merge-base {self.target_branch} HEAD"
print(cmd)
output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
return output.decode(self.encoding).strip()
except subprocess.CalledProcessError as e:
print(f"Error executing git merge-base: {e.output.decode(self.encoding)}")
return None
def get_file_size(self, path: str, ref: str) -> int:
"""获取指定分支上文件的大小"""
try:
# 使用 git cat-file 来获取文件内容,然后计算其大小
cmd = f"git cat-file blob {ref}:{path}"
output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
return len(output)
except subprocess.CalledProcessError:
# 如果文件不存在或无法获取返回0
return 0
def format_size(size: int) -> str:
"""将字节大小转换为人类可读的格式"""
if size >= 0:
if size < 1024:
return f"{size} bytes"
elif size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
elif size < 1024 * 1024 * 1024:
return f"{size / (1024 * 1024):.1f} MB"
else:
return f"{size / (1024 * 1024 * 1024):.1f} GB"
else:
temp_size=abs(size)
if temp_size < 1024:
return f"-{temp_size} bytes"
elif temp_size < 1024 * 1024:
return f"-{temp_size / 1024:.1f} KB"
elif temp_size < 1024 * 1024 * 1024:
return f"-{temp_size / (1024 * 1024):.1f} MB"
else:
return f"-{temp_size / (1024 * 1024 * 1024):.1f} GB"
def is_bsp(path):
return os.path.isfile(os.path.join(path, "rtconfig.h"))
def filter_bsp_config(file_diffs: List[FileDiff], config_path: str):
# 读取原始JSON配置
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# 获取所有修改的文件路径统一使用Linux风格路径
modified_paths = [diff.path.replace('\\', '/') for diff in file_diffs]
print(modified_paths)
if not modified_paths:
print("master分支运行")
return
bsp_paths = set()
bsp_in_but_not_bsp_paths = set()
all_print_paths = set()
for modified_path in modified_paths:
parts = modified_path.strip(os.sep).split('/')
if not parts:
continue
first_level = parts[0]
first_level_path = os.path.join(os.getcwd(), first_level)
#处理bsp路径的逻辑
if first_level == "bsp":
temp_path=os.path.join(os.getcwd(), modified_path)
# 判断是否是文件
if not os.path.isdir(modified_path):
temp_path = os.path.dirname(temp_path)
#循环搜索每一级是否有rtconfig.h
while temp_path !=first_level_path:
if is_bsp(temp_path):
bsp_paths.add(temp_path)
break
temp_path=os.path.dirname(temp_path)
if temp_path ==first_level_path:
bsp_in_but_not_bsp_paths.add(parts[1])
else:
#非bsp路径逻辑
all_print_paths.add(first_level_path)
# 变成相对路径
bsp_list = set()
for path in sorted(bsp_paths):
current_working_dir = os.path.join(os.getcwd(), "bsp/")
if path.startswith(current_working_dir):
bsp_list.add(path[len(current_working_dir):].lstrip(os.sep))
else:
bsp_list.add(path) # 如果 first_level_path 不以 current_working_dir 开头,则保持不变
# 处理修改了bsp外的文件的情况
filtered_bsp = [
path for path in bsp_list
if path.split('/')[0] not in bsp_in_but_not_bsp_paths
]
merged_result = filtered_bsp + list(bsp_in_but_not_bsp_paths)
filtered_legs = []
for leg in config["legs"]:
matched_paths = [
path for path in leg.get("SUB_RTT_BSP", [])
if any(keyword in path for keyword in merged_result)
]
if matched_paths:
filtered_legs.append({**leg, "SUB_RTT_BSP": matched_paths})
# 生成新的配置
new_config = {"legs": filtered_legs}
# 判断有没有修改到bsp外的文件有的话则编译全部
if not all_print_paths:
print(new_config)
file_name = ".github/ALL_BSP_COMPILE_TEMP.json"
# 将 new_config 写入文件
with open(file_name, "w", encoding="utf-8") as file:
json.dump(new_config, file, ensure_ascii=False, indent=4)
def main():
# 源文件路径
source_file = ".github/ALL_BSP_COMPILE.json" # 替换为你的文件路径
# 目标文件路径
target_file = ".github/ALL_BSP_COMPILE_TEMP.json" # 替换为你的目标文件路径
# 读取源文件并过滤掉带有 // 的行
with open(source_file, "r", encoding="utf-8") as infile, open(target_file, "w", encoding="utf-8") as outfile:
for line in infile:
if not line.lstrip().startswith("//"):
outfile.write(line)
parser = argparse.ArgumentParser(description='Compare current branch with target branch and show file differences.')
parser.add_argument('target_branch', help='Target branch to compare against (e.g., master)')
args = parser.parse_args()
analyzer = GitDiffAnalyzer(args.target_branch)
file_diffs = analyzer.get_diff_files()
# 生成报告
generate_report(file_diffs, args.target_branch)
filter_bsp_config(file_diffs,".github/ALL_BSP_COMPILE_TEMP.json")
def add_summary(text):
"""
add summary to github action.
"""
if "GITHUB_STEP_SUMMARY" in os.environ:
summary_path = os.environ["GITHUB_STEP_SUMMARY"] # 获取摘要文件路径
# 将 text 写入摘要文件(追加模式)
with open(summary_path, "a") as f: # "a" 表示追加模式
f.write(text + "\n") # 写入文本并换行
else:
print("Environment variable $GITHUB_STEP_SUMMARY is not set.")
def summarize_diff(label, count, size=None):
"""格式化输出变更摘要"""
line = f" {label:<12} {count:>3} files"
if size is not None:
line += f" ({format_size(size)})"
add_summary(line)
def generate_report(file_diffs: List[FileDiff], target_branch: str):
"""生成差异报告"""
add_summary(f"# 📊 **Comparison between {target_branch} and Current Branch**\n")
# 分类统计
added_files = [f for f in file_diffs if f.status.startswith('A')]
removed_files = [f for f in file_diffs if f.status.startswith('D')]
modified_files = [f for f in file_diffs if f.status.startswith('M')]
renamed_files = [f for f in file_diffs if f.status.startswith('R')]
copied_files = [f for f in file_diffs if f.status.startswith('C')]
unmerged_files = [f for f in file_diffs if f.status.startswith('U')]
type_changed_files = [f for f in file_diffs if f.status.startswith('T')]
# 计算总变化量
total_added = sum(f.size_change for f in added_files)
total_removed = sum(f.old_size for f in removed_files)
total_modified = sum(f.size_change for f in modified_files)
total_copied = sum(f.size_change for f in copied_files)
total_renamed = sum(f.old_size for f in renamed_files)
total_type_changed = sum(f.size_change for f in type_changed_files)
total_size_change = sum(f.size_change for f in file_diffs)
# === 汇总输出 ===
summarize_diff("Total:", len(file_diffs))
summarize_diff("Added:", len(added_files), total_added)
summarize_diff("Removed:", len(removed_files), total_removed)
summarize_diff("Modified:", len(modified_files), total_modified)
summarize_diff("Renamed:", len(renamed_files), total_renamed)
summarize_diff("Copied:", len(copied_files), total_copied)
summarize_diff("Type Changed:", len(type_changed_files), total_type_changed)
summarize_diff("Unmerged:", len(unmerged_files))
if total_size_change > 0:
change_desc = f"📈 **Increase of {format_size(total_size_change)}**"
elif total_size_change < 0:
change_desc = f"📉 **Reduction of {format_size(abs(total_size_change))}**"
else:
change_desc = " **No net size change**"
add_summary(f"\n### 📦 **Total Size Change:** {change_desc} (Excluding removed files)")
# 显示详细差异文件内容
add_summary("\n## 📂 **Detailed File Changes**\n")
for diff in file_diffs:
add_summary(f"📄 {diff.path} — **[{diff.status}]**")
# 文件状态处理
if diff.status.startswith('A'):
add_summary(f"📦 Size: {format_size(diff.new_size)}")
elif diff.status.startswith(('M', 'R')): # 修改或重命名
add_summary(f"📏 Original size: {format_size(diff.old_size)}")
add_summary(f"📐 New size: {format_size(diff.new_size)}")
delta = diff.size_change
if delta > 0:
change_str = f"📈 Increased by {format_size(delta)}"
elif delta < 0:
change_str = f"📉 Reduced by {format_size(abs(delta))}"
else:
change_str = " No size change"
add_summary(f"📊 Size change: {change_str}")
elif diff.status.startswith('D'):
add_summary(f"🗑️ Original size: {format_size(diff.old_size)}")
elif diff.status.startswith('C'):
add_summary(f"📎 Copied from size: {format_size(diff.old_size)}{format_size(diff.new_size)}")
elif diff.status.startswith('T'):
add_summary("⚙️ File type changed")
elif diff.status.startswith('U'):
add_summary("⚠️ Unmerged conflict detected")
else:
add_summary("❓ Unknown change type")
add_summary("\n\n")
if __name__ == "__main__":
main()