git_diff_show.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #
  2. # Copyright (c) 2025, RT-Thread Development Team
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. #
  6. # Change Logs:
  7. # Date Author Notes
  8. # 2025-05-15 supperthomas add PR status show
  9. # 2025-05-22 hydevcode 替换bsp_building.yml的判断文件修改机制,并将PR status show合并进bsp_building.yml
  10. import subprocess
  11. import sys
  12. import os
  13. import re
  14. import argparse
  15. import locale
  16. from typing import List, Dict
  17. import json
  18. from typing import List
  19. class FileDiff:
  20. def __init__(self, path: str, status: str, size_change: int = 0, old_size: int = 0, new_size: int = 0):
  21. self.path = path
  22. self.status = status # A (added), M (modified), D (deleted), R (renamed)
  23. self.size_change = size_change
  24. self.old_size = old_size
  25. self.new_size = new_size
  26. def __str__(self):
  27. if self.status == 'A':
  28. return f"Added {self.path}: {self.size_change} bytes"
  29. elif self.status == 'D':
  30. return f"Deleted {self.path}: was {self.old_size} bytes"
  31. elif self.status == 'M' or self.status == 'R':
  32. return f"Modified {self.path}: {self.size_change} bytes change"
  33. else:
  34. return f"{self.status} {self.path}"
  35. class GitDiffAnalyzer:
  36. def __init__(self, target_branch: str):
  37. self.target_branch = target_branch
  38. self.encoding = locale.getpreferredencoding()
  39. def get_diff_files(self) -> List[FileDiff]:
  40. """获取当前分支与目标分支之间的差异文件"""
  41. # 找到当前分支和目标分支的最近共同祖先
  42. merge_base = self.get_merge_base()
  43. if not merge_base:
  44. print("No common ancestor found between current branch and target branch")
  45. sys.exit(1)
  46. # 获取差异文件列表
  47. diff_cmd = f"git diff --name-status {merge_base} HEAD"
  48. print(diff_cmd)
  49. try:
  50. output = subprocess.check_output(diff_cmd.split(), stderr=subprocess.STDOUT)
  51. output = output.decode(self.encoding).strip()
  52. print(output)
  53. except subprocess.CalledProcessError as e:
  54. print(f"Error executing git diff: {e.output.decode(self.encoding)}")
  55. sys.exit(1)
  56. if not output:
  57. print("No differences between current branch and target branch")
  58. sys.exit(0)
  59. # 处理可能的换行符问题
  60. output = output.replace('\r\n', '\n')
  61. lines = output.split('\n')
  62. file_diffs = []
  63. for line in lines:
  64. line = line.strip()
  65. if not line:
  66. continue
  67. parts = line.split('\t')
  68. if len(parts) < 2:
  69. # 可能是重命名文件,格式为 "RXXX\told_path\tnew_path"
  70. match = re.match(r'R(\d+)\t(.+)\t(.+)', line)
  71. if match:
  72. status = 'R'
  73. old_path = match.group(2)
  74. new_path = match.group(3)
  75. # 计算重命名文件的修改大小
  76. old_size = self.get_file_size(old_path, self.target_branch)
  77. new_size = self.get_file_size(new_path, 'HEAD')
  78. size_change = new_size - old_size if old_size > 0 else new_size
  79. file_diffs.append(FileDiff(new_path, status, size_change, old_size, new_size))
  80. else:
  81. status = parts[0][0] # 取状态码的第一个字符
  82. path = parts[1]
  83. if status == 'A':
  84. # 新增文件,计算大小
  85. size = self.get_file_size(path, 'HEAD')
  86. file_diffs.append(FileDiff(path, status, size, 0, size))
  87. elif status == 'D':
  88. # 删除文件,计算原大小
  89. size = self.get_file_size(path, self.target_branch)
  90. file_diffs.append(FileDiff(path, status, 0, size, 0))
  91. elif status == 'M':
  92. # 修改文件,计算大小变化
  93. old_size = self.get_file_size(path, self.target_branch)
  94. new_size = self.get_file_size(path, 'HEAD')
  95. size_change = new_size - old_size
  96. file_diffs.append(FileDiff(path, status, size_change, old_size, new_size))
  97. return file_diffs
  98. def get_merge_base(self) -> str:
  99. """获取当前分支和目标分支的最近共同祖先"""
  100. try:
  101. cmd = f"git merge-base {self.target_branch} HEAD"
  102. print(cmd)
  103. output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
  104. return output.decode(self.encoding).strip()
  105. except subprocess.CalledProcessError as e:
  106. print(f"Error executing git merge-base: {e.output.decode(self.encoding)}")
  107. return None
  108. def get_file_size(self, path: str, ref: str) -> int:
  109. """获取指定分支上文件的大小"""
  110. try:
  111. # 使用 git cat-file 来获取文件内容,然后计算其大小
  112. cmd = f"git cat-file blob {ref}:{path}"
  113. output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
  114. return len(output)
  115. except subprocess.CalledProcessError:
  116. # 如果文件不存在或无法获取,返回0
  117. return 0
  118. def format_size(size: int) -> str:
  119. """将字节大小转换为人类可读的格式"""
  120. if size >= 0:
  121. if size < 1024:
  122. return f"{size} bytes"
  123. elif size < 1024 * 1024:
  124. return f"{size / 1024:.1f} KB"
  125. elif size < 1024 * 1024 * 1024:
  126. return f"{size / (1024 * 1024):.1f} MB"
  127. else:
  128. return f"{size / (1024 * 1024 * 1024):.1f} GB"
  129. else:
  130. temp_size=abs(size)
  131. if temp_size < 1024:
  132. return f"-{temp_size} bytes"
  133. elif temp_size < 1024 * 1024:
  134. return f"-{temp_size / 1024:.1f} KB"
  135. elif temp_size < 1024 * 1024 * 1024:
  136. return f"-{temp_size / (1024 * 1024):.1f} MB"
  137. else:
  138. return f"-{temp_size / (1024 * 1024 * 1024):.1f} GB"
  139. def is_bsp(path):
  140. return os.path.isfile(os.path.join(path, "rtconfig.h"))
  141. def filter_bsp_config(file_diffs: List[FileDiff], config_path: str):
  142. # 读取原始JSON配置
  143. with open(config_path, 'r', encoding='utf-8') as f:
  144. config = json.load(f)
  145. # 获取所有修改的文件路径(统一使用Linux风格路径)
  146. modified_paths = [diff.path.replace('\\', '/') for diff in file_diffs]
  147. print(modified_paths)
  148. if not modified_paths:
  149. print("master分支运行")
  150. return
  151. bsp_paths = set()
  152. bsp_in_but_not_bsp_paths = set()
  153. all_print_paths = set()
  154. for modified_path in modified_paths:
  155. parts = modified_path.strip(os.sep).split('/')
  156. if not parts:
  157. continue
  158. first_level = parts[0]
  159. first_level_path = os.path.join(os.getcwd(), first_level)
  160. #处理bsp路径的逻辑
  161. if first_level == "bsp":
  162. temp_path=os.path.join(os.getcwd(), modified_path)
  163. # 判断是否是文件
  164. if not os.path.isdir(modified_path):
  165. temp_path = os.path.dirname(temp_path)
  166. #循环搜索每一级是否有rtconfig.h
  167. while temp_path !=first_level_path:
  168. if is_bsp(temp_path):
  169. bsp_paths.add(temp_path)
  170. break
  171. temp_path=os.path.dirname(temp_path)
  172. if temp_path ==first_level_path:
  173. bsp_in_but_not_bsp_paths.add(parts[1])
  174. else:
  175. #非bsp路径逻辑
  176. all_print_paths.add(first_level_path)
  177. # 变成相对路径
  178. bsp_list = set()
  179. for path in sorted(bsp_paths):
  180. current_working_dir = os.path.join(os.getcwd(), "bsp/")
  181. if path.startswith(current_working_dir):
  182. bsp_list.add(path[len(current_working_dir):].lstrip(os.sep))
  183. else:
  184. bsp_list.add(path) # 如果 first_level_path 不以 current_working_dir 开头,则保持不变
  185. # 处理修改了bsp外的文件的情况
  186. filtered_bsp = [
  187. path for path in bsp_list
  188. if path.split('/')[0] not in bsp_in_but_not_bsp_paths
  189. ]
  190. merged_result = filtered_bsp + list(bsp_in_but_not_bsp_paths)
  191. filtered_legs = []
  192. for leg in config["legs"]:
  193. matched_paths = [
  194. path for path in leg.get("SUB_RTT_BSP", [])
  195. if any(keyword in path for keyword in merged_result)
  196. ]
  197. if matched_paths:
  198. filtered_legs.append({**leg, "SUB_RTT_BSP": matched_paths})
  199. # 生成新的配置
  200. new_config = {"legs": filtered_legs}
  201. # 判断有没有修改到bsp外的文件,有的话则编译全部
  202. if not all_print_paths:
  203. print(new_config)
  204. file_name = ".github/ALL_BSP_COMPILE_TEMP.json"
  205. # 将 new_config 写入文件
  206. with open(file_name, "w", encoding="utf-8") as file:
  207. json.dump(new_config, file, ensure_ascii=False, indent=4)
  208. def main():
  209. # 源文件路径
  210. source_file = ".github/ALL_BSP_COMPILE.json" # 替换为你的文件路径
  211. # 目标文件路径
  212. target_file = ".github/ALL_BSP_COMPILE_TEMP.json" # 替换为你的目标文件路径
  213. # 读取源文件并过滤掉带有 // 的行
  214. with open(source_file, "r", encoding="utf-8") as infile, open(target_file, "w", encoding="utf-8") as outfile:
  215. for line in infile:
  216. if not line.lstrip().startswith("//"):
  217. outfile.write(line)
  218. parser = argparse.ArgumentParser(description='Compare current branch with target branch and show file differences.')
  219. parser.add_argument('target_branch', help='Target branch to compare against (e.g., master)')
  220. args = parser.parse_args()
  221. analyzer = GitDiffAnalyzer(args.target_branch)
  222. file_diffs = analyzer.get_diff_files()
  223. # 生成报告
  224. generate_report(file_diffs, args.target_branch)
  225. filter_bsp_config(file_diffs,".github/ALL_BSP_COMPILE_TEMP.json")
  226. def add_summary(text):
  227. """
  228. add summary to github action.
  229. """
  230. if "GITHUB_STEP_SUMMARY" in os.environ:
  231. summary_path = os.environ["GITHUB_STEP_SUMMARY"] # 获取摘要文件路径
  232. # 将 text 写入摘要文件(追加模式)
  233. with open(summary_path, "a") as f: # "a" 表示追加模式
  234. f.write(text + "\n") # 写入文本并换行
  235. else:
  236. print("Environment variable $GITHUB_STEP_SUMMARY is not set.")
  237. def summarize_diff(label, count, size=None):
  238. """格式化输出变更摘要"""
  239. line = f" {label:<12} {count:>3} files"
  240. if size is not None:
  241. line += f" ({format_size(size)})"
  242. add_summary(line)
  243. def generate_report(file_diffs: List[FileDiff], target_branch: str):
  244. """生成差异报告"""
  245. add_summary(f"# 📊 **Comparison between {target_branch} and Current Branch**\n")
  246. # 分类统计
  247. added_files = [f for f in file_diffs if f.status.startswith('A')]
  248. removed_files = [f for f in file_diffs if f.status.startswith('D')]
  249. modified_files = [f for f in file_diffs if f.status.startswith('M')]
  250. renamed_files = [f for f in file_diffs if f.status.startswith('R')]
  251. copied_files = [f for f in file_diffs if f.status.startswith('C')]
  252. unmerged_files = [f for f in file_diffs if f.status.startswith('U')]
  253. type_changed_files = [f for f in file_diffs if f.status.startswith('T')]
  254. # 计算总变化量
  255. total_added = sum(f.size_change for f in added_files)
  256. total_removed = sum(f.old_size for f in removed_files)
  257. total_modified = sum(f.size_change for f in modified_files)
  258. total_copied = sum(f.size_change for f in copied_files)
  259. total_renamed = sum(f.old_size for f in renamed_files)
  260. total_type_changed = sum(f.size_change for f in type_changed_files)
  261. total_size_change = sum(f.size_change for f in file_diffs)
  262. # === 汇总输出 ===
  263. summarize_diff("Total:", len(file_diffs))
  264. summarize_diff("Added:", len(added_files), total_added)
  265. summarize_diff("Removed:", len(removed_files), total_removed)
  266. summarize_diff("Modified:", len(modified_files), total_modified)
  267. summarize_diff("Renamed:", len(renamed_files), total_renamed)
  268. summarize_diff("Copied:", len(copied_files), total_copied)
  269. summarize_diff("Type Changed:", len(type_changed_files), total_type_changed)
  270. summarize_diff("Unmerged:", len(unmerged_files))
  271. if total_size_change > 0:
  272. change_desc = f"📈 **Increase of {format_size(total_size_change)}**"
  273. elif total_size_change < 0:
  274. change_desc = f"📉 **Reduction of {format_size(abs(total_size_change))}**"
  275. else:
  276. change_desc = "➖ **No net size change**"
  277. add_summary(f"\n### 📦 **Total Size Change:** {change_desc} (Excluding removed files)")
  278. # 显示详细差异文件内容
  279. add_summary("\n## 📂 **Detailed File Changes**\n")
  280. for diff in file_diffs:
  281. add_summary(f"📄 {diff.path} — **[{diff.status}]**")
  282. # 文件状态处理
  283. if diff.status.startswith('A'):
  284. add_summary(f"📦 Size: {format_size(diff.new_size)}")
  285. elif diff.status.startswith(('M', 'R')): # 修改或重命名
  286. add_summary(f"📏 Original size: {format_size(diff.old_size)}")
  287. add_summary(f"📐 New size: {format_size(diff.new_size)}")
  288. delta = diff.size_change
  289. if delta > 0:
  290. change_str = f"📈 Increased by {format_size(delta)}"
  291. elif delta < 0:
  292. change_str = f"📉 Reduced by {format_size(abs(delta))}"
  293. else:
  294. change_str = "➖ No size change"
  295. add_summary(f"📊 Size change: {change_str}")
  296. elif diff.status.startswith('D'):
  297. add_summary(f"🗑️ Original size: {format_size(diff.old_size)}")
  298. elif diff.status.startswith('C'):
  299. add_summary(f"📎 Copied from size: {format_size(diff.old_size)} → {format_size(diff.new_size)}")
  300. elif diff.status.startswith('T'):
  301. add_summary("⚙️ File type changed")
  302. elif diff.status.startswith('U'):
  303. add_summary("⚠️ Unmerged conflict detected")
  304. else:
  305. add_summary("❓ Unknown change type")
  306. add_summary("\n\n")
  307. if __name__ == "__main__":
  308. main()