|
| 1 | +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +# flake8: noqa |
| 5 | + |
| 6 | +import os |
| 7 | +import yaml |
| 8 | +import subprocess |
| 9 | +from tabulate import tabulate |
| 10 | +import re |
| 11 | +from datetime import datetime |
| 12 | +import wcwidth |
| 13 | +from pathlib import Path |
| 14 | +import pytz |
| 15 | + |
| 16 | +repo_path = Path(".") |
| 17 | + |
| 18 | +target_dirs = [ |
| 19 | + os.path.join(repo_path, "device"), |
| 20 | + os.path.join(repo_path, "host"), |
| 21 | +] |
| 22 | + |
| 23 | +deprecated = [] |
| 24 | + |
| 25 | +priority_order = { |
| 26 | + "⛔ Yes": 0, |
| 27 | + "⚠️ MD ": 1, |
| 28 | + "✔️ No ": 2 |
| 29 | +} |
| 30 | + |
| 31 | +results = [] |
| 32 | + |
| 33 | +release_commits = {} |
| 34 | +component_paths = {} |
| 35 | + |
| 36 | + |
| 37 | +def run_git_command(args, cwd): |
| 38 | + result = subprocess.run(["git"] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) |
| 39 | + return result.stdout.strip() |
| 40 | + |
| 41 | + |
| 42 | +for base_dir in target_dirs: |
| 43 | + if os.path.exists(base_dir): |
| 44 | + for root, dirs, files in os.walk(base_dir): |
| 45 | + if "idf_component.yml" in files: |
| 46 | + yml_path = os.path.join(root, "idf_component.yml") |
| 47 | + component_name = os.path.basename(root) |
| 48 | + version = "N/A" |
| 49 | + release_date = "?" |
| 50 | + changes_since_version = "N/A" |
| 51 | + commit_count = "?" |
| 52 | + |
| 53 | + if component_name in deprecated: |
| 54 | + continue |
| 55 | + |
| 56 | + try: |
| 57 | + with open(yml_path, "r") as f: |
| 58 | + yml_data = yaml.safe_load(f) |
| 59 | + version = yml_data.get("version", "N/A") |
| 60 | + except Exception as e: |
| 61 | + print(f"Chyba: {e}") |
| 62 | + |
| 63 | + if version != "N/A": |
| 64 | + try: |
| 65 | + rel_yml_path = os.path.relpath(yml_path, repo_path).replace("\\", "/") |
| 66 | + log_output = run_git_command(["log", "-p", "--", rel_yml_path], cwd=repo_path) |
| 67 | + |
| 68 | + current_commit = None |
| 69 | + current_date = None |
| 70 | + old_version = None |
| 71 | + new_version = None |
| 72 | + commit_hash = None |
| 73 | + |
| 74 | + lines = log_output.splitlines() |
| 75 | + for i, line in enumerate(lines): |
| 76 | + if line.startswith("commit "): |
| 77 | + current_commit = line.split()[1] |
| 78 | + old_version = None |
| 79 | + new_version = None |
| 80 | + elif line.startswith("Date:"): |
| 81 | + raw_date = line.replace("Date:", "").strip() |
| 82 | + try: |
| 83 | + dt = datetime.strptime(raw_date, "%a %b %d %H:%M:%S %Y %z") |
| 84 | + current_date = dt.strftime("%d.%m.%Y") |
| 85 | + except Exception as e: |
| 86 | + print(f"Chyba: {e}") |
| 87 | + current_date = raw_date |
| 88 | + elif line.startswith("-version:") and not line.startswith(" "): |
| 89 | + match = re.match(r"-version:\s*['\"]?([\w\.\-~]+)['\"]?", line) |
| 90 | + if match: |
| 91 | + old_version = match.group(1) |
| 92 | + elif line.startswith("+version:") and not line.startswith(" "): |
| 93 | + match = re.match(r"\+version:\s*['\"]?([\w\.\-~]+)['\"]?", line) |
| 94 | + if match: |
| 95 | + new_version = match.group(1) |
| 96 | + |
| 97 | + if old_version and new_version and old_version != new_version: |
| 98 | + commit_hash = current_commit |
| 99 | + release_date = current_date |
| 100 | + break |
| 101 | + |
| 102 | + if not commit_hash: |
| 103 | + first_commit = run_git_command(["log", "--diff-filter=A", "--format=%H %aD", "--", rel_yml_path], cwd=repo_path) |
| 104 | + if first_commit: |
| 105 | + parts = first_commit.split() |
| 106 | + commit_hash = parts[0] |
| 107 | + try: |
| 108 | + dt = datetime.strptime(" ".join(parts[1:]), "%a, %d %b %Y %H:%M:%S %z") |
| 109 | + release_date = dt.strftime("%d.%m.%Y") |
| 110 | + except Exception as e: |
| 111 | + print(f"Chyba: {e}") |
| 112 | + release_date = "?" |
| 113 | + |
| 114 | + if commit_hash: |
| 115 | + rel_component_path = os.path.relpath(root, repo_path).replace("\\", "/") |
| 116 | + |
| 117 | + # Save |
| 118 | + release_commits[component_name] = commit_hash |
| 119 | + component_paths[component_name] = rel_component_path |
| 120 | + |
| 121 | + diff_output = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_component_path], cwd=repo_path) |
| 122 | + |
| 123 | + extensions = {} |
| 124 | + changed_paths = diff_output.splitlines() |
| 125 | + if diff_output: |
| 126 | + extensions = {os.path.splitext(path)[1] for path in changed_paths} |
| 127 | + |
| 128 | + if extensions == {'.md'}: |
| 129 | + changes_since_version = "⚠️ MarkDown " |
| 130 | + elif changed_paths and all("test_apps/" in path for path in changed_paths): |
| 131 | + changes_since_version = "🧪 Test only" |
| 132 | + elif extensions: |
| 133 | + changes_since_version = "⛔ Yes" |
| 134 | + else: |
| 135 | + changes_since_version = "✔️ No " |
| 136 | + |
| 137 | + # count_output = run_git_command(["rev-list", f"{commit_hash}..HEAD", "--count", rel_component_path], cwd=repo_path) |
| 138 | + commit_count = len(changed_paths) if changed_paths else "?" |
| 139 | + |
| 140 | + except Exception as e: |
| 141 | + print(f"Chyba: {e}") |
| 142 | + |
| 143 | + if release_date != "?": |
| 144 | + extension_str = ", ".join(sorted(extensions)) if extensions else " " |
| 145 | + results.append([component_name, version, release_date, changes_since_version + f" ({commit_count})", extension_str]) |
| 146 | + |
| 147 | + |
| 148 | +def show_diff_for_component(component_name): |
| 149 | + commit_hash = release_commits.get(component_name) |
| 150 | + rel_path = component_paths.get(component_name) |
| 151 | + |
| 152 | + if not commit_hash or not rel_path: |
| 153 | + print("Commit path not found.") |
| 154 | + return |
| 155 | + |
| 156 | + # List of changed files |
| 157 | + changed_files = run_git_command(["diff", "--name-only", f"{commit_hash}..HEAD", "--", rel_path], cwd=repo_path) |
| 158 | + changed_files = [f for f in changed_files.splitlines() if not f.endswith(".md")] |
| 159 | + |
| 160 | + if not changed_files: |
| 161 | + print("No changes except *.md files.") |
| 162 | + return |
| 163 | + |
| 164 | + print(f"Changes for component '{component_name}' from last release (except *.md files):\n") |
| 165 | + subprocess.run(["git", "diff", "--color=always", f"{commit_hash}..HEAD", "--"] + changed_files, cwd=repo_path) |
| 166 | + |
| 167 | + |
| 168 | +# Calculate width |
| 169 | +def visual_width(text): |
| 170 | + return sum(wcwidth.wcwidth(c) for c in text) |
| 171 | + |
| 172 | + |
| 173 | +# Text align |
| 174 | +def pad_visual(text, target_width): |
| 175 | + current_width = visual_width(text) |
| 176 | + padding = max(0, target_width - current_width) |
| 177 | + return text + " " * padding |
| 178 | + |
| 179 | + |
| 180 | +def get_change_key(row): |
| 181 | + change = row[3].strip() |
| 182 | + extensions = row[4].split(", ") |
| 183 | + has_code_change = any(ext in ['.c', '.h'] for ext in extensions) |
| 184 | + |
| 185 | + if change.startswith("⛔") and has_code_change: |
| 186 | + return 0 |
| 187 | + elif change.startswith("⛔"): |
| 188 | + return 1 |
| 189 | + elif change.startswith("🧪"): |
| 190 | + return 2 |
| 191 | + elif change.startswith("⚠️"): |
| 192 | + return 3 |
| 193 | + elif change.startswith("✔️"): |
| 194 | + return 4 |
| 195 | + return 99 |
| 196 | + |
| 197 | + |
| 198 | +# Sort by priority |
| 199 | +results.sort(key=get_change_key) |
| 200 | + |
| 201 | +# Column align |
| 202 | +for row in results: |
| 203 | + row[3] = pad_visual(row[3], 8) |
| 204 | + |
| 205 | +# Table header |
| 206 | +headers = ["Component", "Version", "Released", "Changed", "File Types"] |
| 207 | + |
| 208 | +tz = pytz.timezone("Europe/Prague") |
| 209 | +last_updated = datetime.now(tz).strftime("%d.%m.%Y %H:%M:%S %Z") |
| 210 | +if os.getenv("CI") != "true": |
| 211 | + markdown_table = tabulate(results, headers=headers, tablefmt="github") |
| 212 | + print("# Component/BSP release version checker") |
| 213 | + print("This page shows all components in the BSP repository with their latest versions and indicates whether any changes have not yet been released.") |
| 214 | +else: |
| 215 | + markdown_table = tabulate(results, headers=headers, tablefmt="html") |
| 216 | + deprecated_str = ", ".join(deprecated) |
| 217 | + print("<html><head>") |
| 218 | + print("<title>Component/BSP release version checker</title>") |
| 219 | + print(f"""<style> |
| 220 | + body {{ font-family: sans-serif; padding: 2em; }} |
| 221 | + table {{ border-collapse: collapse; width: 100%; }} |
| 222 | + th, td {{ border: 1px solid #ccc; padding: 0.5em; text-align: left; }} |
| 223 | + th {{ background-color: #f0f0f0; }} |
| 224 | + td:nth-child(4) {{ text-align: center; }} |
| 225 | + td:nth-child(5) {{ font-style: italic; color: #888; }} |
| 226 | + </style>""") |
| 227 | + print("</head><body>") |
| 228 | + print("<h1>Component/BSP release version checker</h1>") |
| 229 | + print(f"<p>Last updated: {last_updated}</p>") |
| 230 | + print("<p>This page shows all components in the BSP repository with their latest versions and indicates whether any changes have not yet been released.</p>") |
| 231 | + print(f"<p>Deprecated components: {deprecated_str}</p>") |
| 232 | + print("</body></html>") |
| 233 | + |
| 234 | +print(markdown_table) |
| 235 | + |
| 236 | +if os.getenv("CI") != "true": |
| 237 | + while True: |
| 238 | + component_name = input("Input the component name for diff (or type 'exit' to quit): ") |
| 239 | + if component_name.lower() == 'exit': |
| 240 | + break |
| 241 | + show_diff_for_component(component_name) |
0 commit comments