1*210a923aSMauro Carvalho Chehab#!/usr/bin/env python3 2*210a923aSMauro Carvalho Chehab# SPDX-License-Identifier: GPL-2.0 3*210a923aSMauro Carvalho Chehab# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>. 4*210a923aSMauro Carvalho Chehab# 5*210a923aSMauro Carvalho Chehab# pylint: disable=R0903,R0912,R0913,R0914,R0915,R0917 6*210a923aSMauro Carvalho Chehab 7*210a923aSMauro Carvalho Chehab""" 8*210a923aSMauro Carvalho Chehabdocdiff - Check differences between kernel‑doc output between two different 9*210a923aSMauro Carvalho Chehabcommits. 10*210a923aSMauro Carvalho Chehab 11*210a923aSMauro Carvalho ChehabExamples 12*210a923aSMauro Carvalho Chehab-------- 13*210a923aSMauro Carvalho Chehab 14*210a923aSMauro Carvalho ChehabCompare the kernel‑doc output between the last two 5.15 releases:: 15*210a923aSMauro Carvalho Chehab 16*210a923aSMauro Carvalho Chehab $ kdoc_diff v6.18..v6.19 17*210a923aSMauro Carvalho Chehab 18*210a923aSMauro Carvalho ChehabBoth outputs are cached 19*210a923aSMauro Carvalho Chehab 20*210a923aSMauro Carvalho ChehabForce a complete documentation scan and clean any previous cache from 21*210a923aSMauro Carvalho Chehab6.19 to the current HEAD:: 22*210a923aSMauro Carvalho Chehab 23*210a923aSMauro Carvalho Chehab $ kdoc_diff 6.19.. --full --clean 24*210a923aSMauro Carvalho Chehab 25*210a923aSMauro Carvalho ChehabCheck differences only on a single driver since origin/main:: 26*210a923aSMauro Carvalho Chehab 27*210a923aSMauro Carvalho Chehab $ kdoc_diff origin/main drivers/media 28*210a923aSMauro Carvalho Chehab 29*210a923aSMauro Carvalho ChehabGenerate an YAML file and use it to check for regressions:: 30*210a923aSMauro Carvalho Chehab 31*210a923aSMauro Carvalho Chehab $ kdoc_diff HEAD~ drivers/media --regression 32*210a923aSMauro Carvalho Chehab 33*210a923aSMauro Carvalho Chehab 34*210a923aSMauro Carvalho Chehab""" 35*210a923aSMauro Carvalho Chehab 36*210a923aSMauro Carvalho Chehabimport os 37*210a923aSMauro Carvalho Chehabimport sys 38*210a923aSMauro Carvalho Chehabimport argparse 39*210a923aSMauro Carvalho Chehabimport subprocess 40*210a923aSMauro Carvalho Chehabimport shutil 41*210a923aSMauro Carvalho Chehabimport re 42*210a923aSMauro Carvalho Chehabimport signal 43*210a923aSMauro Carvalho Chehab 44*210a923aSMauro Carvalho Chehabfrom glob import iglob 45*210a923aSMauro Carvalho Chehab 46*210a923aSMauro Carvalho Chehab 47*210a923aSMauro Carvalho ChehabSRC_DIR = os.path.dirname(os.path.realpath(__file__)) 48*210a923aSMauro Carvalho ChehabWORK_DIR = os.path.abspath(os.path.join(SRC_DIR, "../..")) 49*210a923aSMauro Carvalho Chehab 50*210a923aSMauro Carvalho ChehabKDOC_BINARY = os.path.join(SRC_DIR, "kernel-doc") 51*210a923aSMauro Carvalho ChehabKDOC_PARSER_TEST = os.path.join(WORK_DIR, "tools/unittests/test_kdoc_parser.py") 52*210a923aSMauro Carvalho Chehab 53*210a923aSMauro Carvalho ChehabCACHE_DIR = ".doc_diff_cache" 54*210a923aSMauro Carvalho ChehabYAML_NAME = "out.yaml" 55*210a923aSMauro Carvalho Chehab 56*210a923aSMauro Carvalho ChehabDIR_NAME = { 57*210a923aSMauro Carvalho Chehab "full": os.path.join(CACHE_DIR, "full"), 58*210a923aSMauro Carvalho Chehab "partial": os.path.join(CACHE_DIR, "partial"), 59*210a923aSMauro Carvalho Chehab "no-cache": os.path.join(CACHE_DIR, "no_cache"), 60*210a923aSMauro Carvalho Chehab "tmp": os.path.join(CACHE_DIR, "__tmp__"), 61*210a923aSMauro Carvalho Chehab} 62*210a923aSMauro Carvalho Chehab 63*210a923aSMauro Carvalho Chehabclass GitHelper: 64*210a923aSMauro Carvalho Chehab """Handles all Git operations""" 65*210a923aSMauro Carvalho Chehab 66*210a923aSMauro Carvalho Chehab def __init__(self, work_dir=None): 67*210a923aSMauro Carvalho Chehab self.work_dir = work_dir 68*210a923aSMauro Carvalho Chehab 69*210a923aSMauro Carvalho Chehab def is_inside_repository(self): 70*210a923aSMauro Carvalho Chehab """Check if we're inside a Git repository""" 71*210a923aSMauro Carvalho Chehab try: 72*210a923aSMauro Carvalho Chehab output = subprocess.check_output(["git", "rev-parse", 73*210a923aSMauro Carvalho Chehab "--is-inside-work-tree"], 74*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 75*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT, 76*210a923aSMauro Carvalho Chehab universal_newlines=True) 77*210a923aSMauro Carvalho Chehab 78*210a923aSMauro Carvalho Chehab return output.strip() == "true" 79*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 80*210a923aSMauro Carvalho Chehab return False 81*210a923aSMauro Carvalho Chehab 82*210a923aSMauro Carvalho Chehab def is_valid_commit(self, commit_hash): 83*210a923aSMauro Carvalho Chehab """ 84*210a923aSMauro Carvalho Chehab Validate that a ref (branch, tag, commit hash, etc.) can be 85*210a923aSMauro Carvalho Chehab resolved to a commit. 86*210a923aSMauro Carvalho Chehab """ 87*210a923aSMauro Carvalho Chehab try: 88*210a923aSMauro Carvalho Chehab subprocess.check_output(["git", "rev-parse", commit_hash], 89*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 90*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT) 91*210a923aSMauro Carvalho Chehab return True 92*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 93*210a923aSMauro Carvalho Chehab return False 94*210a923aSMauro Carvalho Chehab 95*210a923aSMauro Carvalho Chehab def get_short_hash(self, commit_hash): 96*210a923aSMauro Carvalho Chehab """Get short commit hash""" 97*210a923aSMauro Carvalho Chehab try: 98*210a923aSMauro Carvalho Chehab return subprocess.check_output(["git", "rev-parse", "--short", 99*210a923aSMauro Carvalho Chehab commit_hash], 100*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 101*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT, 102*210a923aSMauro Carvalho Chehab universal_newlines=True).strip() 103*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 104*210a923aSMauro Carvalho Chehab return "" 105*210a923aSMauro Carvalho Chehab 106*210a923aSMauro Carvalho Chehab def has_uncommitted_changes(self): 107*210a923aSMauro Carvalho Chehab """Check for uncommitted changes""" 108*210a923aSMauro Carvalho Chehab try: 109*210a923aSMauro Carvalho Chehab subprocess.check_output(["git", "diff-index", 110*210a923aSMauro Carvalho Chehab "--quiet", "HEAD", "--"], 111*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 112*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT) 113*210a923aSMauro Carvalho Chehab return False 114*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 115*210a923aSMauro Carvalho Chehab return True 116*210a923aSMauro Carvalho Chehab 117*210a923aSMauro Carvalho Chehab def get_current_branch(self): 118*210a923aSMauro Carvalho Chehab """Get current branch name""" 119*210a923aSMauro Carvalho Chehab return subprocess.check_output(["git", "branch", "--show-current"], 120*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 121*210a923aSMauro Carvalho Chehab universal_newlines=True).strip() 122*210a923aSMauro Carvalho Chehab 123*210a923aSMauro Carvalho Chehab def checkout_commit(self, commit_hash, quiet=True): 124*210a923aSMauro Carvalho Chehab """Checkout a commit safely""" 125*210a923aSMauro Carvalho Chehab args = ["git", "checkout", "-f"] 126*210a923aSMauro Carvalho Chehab if quiet: 127*210a923aSMauro Carvalho Chehab args.append("-q") 128*210a923aSMauro Carvalho Chehab args.append(commit_hash) 129*210a923aSMauro Carvalho Chehab try: 130*210a923aSMauro Carvalho Chehab subprocess.check_output(args, cwd=self.work_dir, 131*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT) 132*210a923aSMauro Carvalho Chehab 133*210a923aSMauro Carvalho Chehab # Double-check if branch actually switched 134*210a923aSMauro Carvalho Chehab branch = self.get_short_hash("HEAD") 135*210a923aSMauro Carvalho Chehab if commit_hash != branch: 136*210a923aSMauro Carvalho Chehab raise RuntimeError(f"Branch changed to '{branch}' instead of '{commit_hash}'") 137*210a923aSMauro Carvalho Chehab 138*210a923aSMauro Carvalho Chehab return True 139*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError as e: 140*210a923aSMauro Carvalho Chehab print(f"ERROR: Failed to checkout {commit_hash}: {e}", 141*210a923aSMauro Carvalho Chehab file=sys.stderr) 142*210a923aSMauro Carvalho Chehab return False 143*210a923aSMauro Carvalho Chehab 144*210a923aSMauro Carvalho Chehab 145*210a923aSMauro Carvalho Chehabclass CacheManager: 146*210a923aSMauro Carvalho Chehab """Manages persistent cache directories""" 147*210a923aSMauro Carvalho Chehab 148*210a923aSMauro Carvalho Chehab def __init__(self, work_dir): 149*210a923aSMauro Carvalho Chehab self.work_dir = work_dir 150*210a923aSMauro Carvalho Chehab 151*210a923aSMauro Carvalho Chehab def initialize(self): 152*210a923aSMauro Carvalho Chehab """Create cache directories if they don't exist""" 153*210a923aSMauro Carvalho Chehab for dir_path in DIR_NAME.values(): 154*210a923aSMauro Carvalho Chehab abs_path = os.path.join(self.work_dir, dir_path) 155*210a923aSMauro Carvalho Chehab if not os.path.exists(abs_path): 156*210a923aSMauro Carvalho Chehab os.makedirs(abs_path, exist_ok=True, mode=0o755) 157*210a923aSMauro Carvalho Chehab 158*210a923aSMauro Carvalho Chehab def get_commit_cache(self, commit_hash, path): 159*210a923aSMauro Carvalho Chehab """Generate cache path for a commit""" 160*210a923aSMauro Carvalho Chehab hash_short = GitHelper(self.work_dir).get_short_hash(commit_hash) 161*210a923aSMauro Carvalho Chehab if not hash_short: 162*210a923aSMauro Carvalho Chehab hash_short = commit_hash 163*210a923aSMauro Carvalho Chehab 164*210a923aSMauro Carvalho Chehab return os.path.join(path, hash_short) 165*210a923aSMauro Carvalho Chehab 166*210a923aSMauro Carvalho Chehabclass KernelDocRunner: 167*210a923aSMauro Carvalho Chehab """Runs kernel-doc documentation generator""" 168*210a923aSMauro Carvalho Chehab 169*210a923aSMauro Carvalho Chehab def __init__(self, work_dir, kdoc_binary): 170*210a923aSMauro Carvalho Chehab self.work_dir = work_dir 171*210a923aSMauro Carvalho Chehab self.kdoc_binary = kdoc_binary 172*210a923aSMauro Carvalho Chehab self.kdoc_files = None 173*210a923aSMauro Carvalho Chehab 174*210a923aSMauro Carvalho Chehab def find_kdoc_references(self): 175*210a923aSMauro Carvalho Chehab """Find all files marked with kernel-doc:: directives""" 176*210a923aSMauro Carvalho Chehab if self.kdoc_files: 177*210a923aSMauro Carvalho Chehab print("Using cached Kdoc refs") 178*210a923aSMauro Carvalho Chehab return self.kdoc_files 179*210a923aSMauro Carvalho Chehab 180*210a923aSMauro Carvalho Chehab print("Finding kernel-doc entries in Documentation...") 181*210a923aSMauro Carvalho Chehab 182*210a923aSMauro Carvalho Chehab files = os.path.join(self.work_dir, 'Documentation/**/*.rst') 183*210a923aSMauro Carvalho Chehab pattern = re.compile(r"^\.\.\s+kernel-doc::\s*(\S+)") 184*210a923aSMauro Carvalho Chehab kdoc_files = set() 185*210a923aSMauro Carvalho Chehab 186*210a923aSMauro Carvalho Chehab for file_path in iglob(files, recursive=True): 187*210a923aSMauro Carvalho Chehab try: 188*210a923aSMauro Carvalho Chehab with open(file_path, 'r', encoding='utf-8') as fp: 189*210a923aSMauro Carvalho Chehab for line in fp: 190*210a923aSMauro Carvalho Chehab match = pattern.match(line.strip()) 191*210a923aSMauro Carvalho Chehab if match: 192*210a923aSMauro Carvalho Chehab kdoc_files.add(match.group(1)) 193*210a923aSMauro Carvalho Chehab 194*210a923aSMauro Carvalho Chehab except OSError: 195*210a923aSMauro Carvalho Chehab continue 196*210a923aSMauro Carvalho Chehab 197*210a923aSMauro Carvalho Chehab self.kdoc_files = list(kdoc_files) 198*210a923aSMauro Carvalho Chehab 199*210a923aSMauro Carvalho Chehab return self.kdoc_files 200*210a923aSMauro Carvalho Chehab 201*210a923aSMauro Carvalho Chehab def gen_yaml(self, yaml_file, kdoc_files): 202*210a923aSMauro Carvalho Chehab """Runs kernel-doc to generate a yaml file with man and rst.""" 203*210a923aSMauro Carvalho Chehab cmd = [self.kdoc_binary, "--man", "--rst", "--yaml", yaml_file] 204*210a923aSMauro Carvalho Chehab cmd += kdoc_files 205*210a923aSMauro Carvalho Chehab 206*210a923aSMauro Carvalho Chehab print(f"YAML regression test file will be stored at: {yaml_file}") 207*210a923aSMauro Carvalho Chehab 208*210a923aSMauro Carvalho Chehab try: 209*210a923aSMauro Carvalho Chehab subprocess.check_call(cmd, cwd=self.work_dir, 210*210a923aSMauro Carvalho Chehab stdout=subprocess.DEVNULL, 211*210a923aSMauro Carvalho Chehab stderr=subprocess.DEVNULL) 212*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 213*210a923aSMauro Carvalho Chehab return False 214*210a923aSMauro Carvalho Chehab 215*210a923aSMauro Carvalho Chehab return True 216*210a923aSMauro Carvalho Chehab 217*210a923aSMauro Carvalho Chehab def run_unittest(self, yaml_file): 218*210a923aSMauro Carvalho Chehab """Run unit tests with the generated yaml file""" 219*210a923aSMauro Carvalho Chehab cmd = [KDOC_PARSER_TEST, "-q", "--yaml", yaml_file] 220*210a923aSMauro Carvalho Chehab result = subprocess.run(cmd, cwd=self.work_dir) 221*210a923aSMauro Carvalho Chehab 222*210a923aSMauro Carvalho Chehab if result.returncode: 223*210a923aSMauro Carvalho Chehab print("To check for problems, try to run it again with -v\n") 224*210a923aSMauro Carvalho Chehab print("Use -k <regex> to filter results\n\n\t$", end="") 225*210a923aSMauro Carvalho Chehab print(" ".join(cmd) + "\n") 226*210a923aSMauro Carvalho Chehab 227*210a923aSMauro Carvalho Chehab return True 228*210a923aSMauro Carvalho Chehab 229*210a923aSMauro Carvalho Chehab def normal_run(self, tmp_dir, output_dir, kdoc_files): 230*210a923aSMauro Carvalho Chehab """Generate man, rst and errors, storing them at tmp_dir.""" 231*210a923aSMauro Carvalho Chehab os.makedirs(tmp_dir, exist_ok=True) 232*210a923aSMauro Carvalho Chehab 233*210a923aSMauro Carvalho Chehab try: 234*210a923aSMauro Carvalho Chehab with open(os.path.join(tmp_dir, "man.log"), "w", encoding="utf-8") as out: 235*210a923aSMauro Carvalho Chehab subprocess.check_call([self.kdoc_binary, "--man"] + kdoc_files, 236*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 237*210a923aSMauro Carvalho Chehab stdout=out, stderr=subprocess.DEVNULL) 238*210a923aSMauro Carvalho Chehab 239*210a923aSMauro Carvalho Chehab with open(os.path.join(tmp_dir, "rst.log"), "w", encoding="utf-8") as out: 240*210a923aSMauro Carvalho Chehab with open(os.path.join(tmp_dir, "err.log"), "w", encoding="utf-8") as err: 241*210a923aSMauro Carvalho Chehab subprocess.check_call([self.kdoc_binary, "--rst"] + kdoc_files, 242*210a923aSMauro Carvalho Chehab cwd=self.work_dir, 243*210a923aSMauro Carvalho Chehab stdout=out, stderr=err) 244*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError: 245*210a923aSMauro Carvalho Chehab return False 246*210a923aSMauro Carvalho Chehab 247*210a923aSMauro Carvalho Chehab if output_dir: 248*210a923aSMauro Carvalho Chehab os.replace(tmp_dir, output_dir) 249*210a923aSMauro Carvalho Chehab 250*210a923aSMauro Carvalho Chehab return True 251*210a923aSMauro Carvalho Chehab 252*210a923aSMauro Carvalho Chehab def run(self, commit_hash, tmp_dir, output_dir, kdoc_files, is_regression, 253*210a923aSMauro Carvalho Chehab is_end): 254*210a923aSMauro Carvalho Chehab """Run kernel-doc on its several ways""" 255*210a923aSMauro Carvalho Chehab if not kdoc_files: 256*210a923aSMauro Carvalho Chehab raise RuntimeError("No kernel-doc references found") 257*210a923aSMauro Carvalho Chehab 258*210a923aSMauro Carvalho Chehab git_helper = GitHelper(self.work_dir) 259*210a923aSMauro Carvalho Chehab if not git_helper.checkout_commit(commit_hash, quiet=True): 260*210a923aSMauro Carvalho Chehab raise RuntimeError(f"ERROR: can't checkout commit {commit_hash}") 261*210a923aSMauro Carvalho Chehab 262*210a923aSMauro Carvalho Chehab print(f"Processing {commit_hash}...") 263*210a923aSMauro Carvalho Chehab 264*210a923aSMauro Carvalho Chehab if not is_regression: 265*210a923aSMauro Carvalho Chehab return self.normal_run(tmp_dir, output_dir, kdoc_files) 266*210a923aSMauro Carvalho Chehab 267*210a923aSMauro Carvalho Chehab yaml_file = os.path.join(tmp_dir, YAML_NAME) 268*210a923aSMauro Carvalho Chehab 269*210a923aSMauro Carvalho Chehab if not is_end: 270*210a923aSMauro Carvalho Chehab return self.gen_yaml(yaml_file, kdoc_files) 271*210a923aSMauro Carvalho Chehab 272*210a923aSMauro Carvalho Chehab return self.run_unittest(yaml_file) 273*210a923aSMauro Carvalho Chehab 274*210a923aSMauro Carvalho Chehabclass DiffManager: 275*210a923aSMauro Carvalho Chehab """Compare documentation output directories with an external diff.""" 276*210a923aSMauro Carvalho Chehab def __init__(self, diff_tool="diff", diff_args=None): 277*210a923aSMauro Carvalho Chehab self.diff_tool = diff_tool 278*210a923aSMauro Carvalho Chehab # default: unified, no context, ignore whitespace changes 279*210a923aSMauro Carvalho Chehab self.diff_args = diff_args or ["-u0", "-w"] 280*210a923aSMauro Carvalho Chehab 281*210a923aSMauro Carvalho Chehab def diff_directories(self, dir1, dir2): 282*210a923aSMauro Carvalho Chehab """Compare two directories using an external diff.""" 283*210a923aSMauro Carvalho Chehab print(f"\nDiffing {dir1} and {dir2}:") 284*210a923aSMauro Carvalho Chehab 285*210a923aSMauro Carvalho Chehab dir1_files = set() 286*210a923aSMauro Carvalho Chehab dir2_files = set() 287*210a923aSMauro Carvalho Chehab has_diff = False 288*210a923aSMauro Carvalho Chehab 289*210a923aSMauro Carvalho Chehab for root, _, files in os.walk(dir1): 290*210a923aSMauro Carvalho Chehab for file in files: 291*210a923aSMauro Carvalho Chehab dir1_files.add(os.path.relpath(os.path.join(root, file), dir1)) 292*210a923aSMauro Carvalho Chehab for root, _, files in os.walk(dir2): 293*210a923aSMauro Carvalho Chehab for file in files: 294*210a923aSMauro Carvalho Chehab dir2_files.add(os.path.relpath(os.path.join(root, file), dir2)) 295*210a923aSMauro Carvalho Chehab 296*210a923aSMauro Carvalho Chehab common_files = sorted(dir1_files & dir2_files) 297*210a923aSMauro Carvalho Chehab for file in common_files: 298*210a923aSMauro Carvalho Chehab f1 = os.path.join(dir1, file) 299*210a923aSMauro Carvalho Chehab f2 = os.path.join(dir2, file) 300*210a923aSMauro Carvalho Chehab 301*210a923aSMauro Carvalho Chehab cmd = [self.diff_tool] + self.diff_args + [f1, f2] 302*210a923aSMauro Carvalho Chehab try: 303*210a923aSMauro Carvalho Chehab result = subprocess.run( 304*210a923aSMauro Carvalho Chehab cmd, capture_output=True, text=True, check=False 305*210a923aSMauro Carvalho Chehab ) 306*210a923aSMauro Carvalho Chehab if result.stdout: 307*210a923aSMauro Carvalho Chehab has_diff = True 308*210a923aSMauro Carvalho Chehab print(f"\n{file}") 309*210a923aSMauro Carvalho Chehab print(result.stdout, end="") 310*210a923aSMauro Carvalho Chehab except FileNotFoundError: 311*210a923aSMauro Carvalho Chehab print(f"ERROR: {self.diff_tool} not found") 312*210a923aSMauro Carvalho Chehab sys.exit(1) 313*210a923aSMauro Carvalho Chehab 314*210a923aSMauro Carvalho Chehab # Show files that exist only in one directory 315*210a923aSMauro Carvalho Chehab only_in_dir1 = dir1_files - dir2_files 316*210a923aSMauro Carvalho Chehab only_in_dir2 = dir2_files - dir1_files 317*210a923aSMauro Carvalho Chehab if only_in_dir1 or only_in_dir2: 318*210a923aSMauro Carvalho Chehab has_diff = True 319*210a923aSMauro Carvalho Chehab print("\nDifferential files:") 320*210a923aSMauro Carvalho Chehab for f in sorted(only_in_dir1): 321*210a923aSMauro Carvalho Chehab print(f" - {f} (only in {dir1})") 322*210a923aSMauro Carvalho Chehab for f in sorted(only_in_dir2): 323*210a923aSMauro Carvalho Chehab print(f" + {f} (only in {dir2})") 324*210a923aSMauro Carvalho Chehab 325*210a923aSMauro Carvalho Chehab if not has_diff: 326*210a923aSMauro Carvalho Chehab print("\nNo differences between those two commits") 327*210a923aSMauro Carvalho Chehab 328*210a923aSMauro Carvalho Chehab 329*210a923aSMauro Carvalho Chehabclass SignalHandler(): 330*210a923aSMauro Carvalho Chehab """Signal handler class.""" 331*210a923aSMauro Carvalho Chehab 332*210a923aSMauro Carvalho Chehab def restore(self, force_exit=False): 333*210a923aSMauro Carvalho Chehab """Restore original HEAD state.""" 334*210a923aSMauro Carvalho Chehab if self.restored: 335*210a923aSMauro Carvalho Chehab return 336*210a923aSMauro Carvalho Chehab 337*210a923aSMauro Carvalho Chehab print(f"Restoring original branch: {self.original_head}") 338*210a923aSMauro Carvalho Chehab try: 339*210a923aSMauro Carvalho Chehab subprocess.check_call( 340*210a923aSMauro Carvalho Chehab ["git", "checkout", "-f", self.original_head], 341*210a923aSMauro Carvalho Chehab cwd=self.git_helper.work_dir, 342*210a923aSMauro Carvalho Chehab stderr=subprocess.STDOUT, 343*210a923aSMauro Carvalho Chehab ) 344*210a923aSMauro Carvalho Chehab except subprocess.CalledProcessError as e: 345*210a923aSMauro Carvalho Chehab print(f"Failed to restore: {e}", file=sys.stderr) 346*210a923aSMauro Carvalho Chehab 347*210a923aSMauro Carvalho Chehab for sig, handler in self.old_handler.items(): 348*210a923aSMauro Carvalho Chehab signal.signal(sig, handler) 349*210a923aSMauro Carvalho Chehab 350*210a923aSMauro Carvalho Chehab self.restored = True 351*210a923aSMauro Carvalho Chehab 352*210a923aSMauro Carvalho Chehab if force_exit: 353*210a923aSMauro Carvalho Chehab sys.exit(1) 354*210a923aSMauro Carvalho Chehab 355*210a923aSMauro Carvalho Chehab def signal_handler(self, sig, _): 356*210a923aSMauro Carvalho Chehab """Handle interrupt signals.""" 357*210a923aSMauro Carvalho Chehab print(f"\nSignal {sig} received. Restoring original state...") 358*210a923aSMauro Carvalho Chehab 359*210a923aSMauro Carvalho Chehab self.restore(force_exit=True) 360*210a923aSMauro Carvalho Chehab 361*210a923aSMauro Carvalho Chehab def __enter__(self): 362*210a923aSMauro Carvalho Chehab """Allow using it via with command.""" 363*210a923aSMauro Carvalho Chehab for sig in [signal.SIGINT, signal.SIGTERM]: 364*210a923aSMauro Carvalho Chehab self.old_handler[sig] = signal.getsignal(sig) 365*210a923aSMauro Carvalho Chehab signal.signal(sig, self.signal_handler) 366*210a923aSMauro Carvalho Chehab 367*210a923aSMauro Carvalho Chehab return self 368*210a923aSMauro Carvalho Chehab 369*210a923aSMauro Carvalho Chehab def __exit__(self, *args): 370*210a923aSMauro Carvalho Chehab """Restore signals at the end of with block.""" 371*210a923aSMauro Carvalho Chehab self.restore() 372*210a923aSMauro Carvalho Chehab 373*210a923aSMauro Carvalho Chehab def __init__(self, git_helper, original_head): 374*210a923aSMauro Carvalho Chehab self.git_helper = git_helper 375*210a923aSMauro Carvalho Chehab self.original_head = original_head 376*210a923aSMauro Carvalho Chehab self.old_handler = {} 377*210a923aSMauro Carvalho Chehab self.restored = False 378*210a923aSMauro Carvalho Chehab 379*210a923aSMauro Carvalho Chehabdef parse_commit_range(value): 380*210a923aSMauro Carvalho Chehab """Handle a commit range.""" 381*210a923aSMauro Carvalho Chehab if ".." not in value: 382*210a923aSMauro Carvalho Chehab begin = value 383*210a923aSMauro Carvalho Chehab end = "HEAD" 384*210a923aSMauro Carvalho Chehab else: 385*210a923aSMauro Carvalho Chehab begin, _, end = value.partition("..") 386*210a923aSMauro Carvalho Chehab if not end: 387*210a923aSMauro Carvalho Chehab end = "HEAD" 388*210a923aSMauro Carvalho Chehab 389*210a923aSMauro Carvalho Chehab if not begin: 390*210a923aSMauro Carvalho Chehab raise argparse.ArgumentTypeError("Need a commit begginning") 391*210a923aSMauro Carvalho Chehab 392*210a923aSMauro Carvalho Chehab 393*210a923aSMauro Carvalho Chehab print(f"Range: {begin} to {end}") 394*210a923aSMauro Carvalho Chehab 395*210a923aSMauro Carvalho Chehab return begin, end 396*210a923aSMauro Carvalho Chehab 397*210a923aSMauro Carvalho Chehab 398*210a923aSMauro Carvalho Chehabdef main(): 399*210a923aSMauro Carvalho Chehab """Main code""" 400*210a923aSMauro Carvalho Chehab parser = argparse.ArgumentParser(description="Compare kernel documentation between commits") 401*210a923aSMauro Carvalho Chehab parser.add_argument("commits", type=parse_commit_range, 402*210a923aSMauro Carvalho Chehab help="commit range like old..new") 403*210a923aSMauro Carvalho Chehab parser.add_argument("files", nargs="*", 404*210a923aSMauro Carvalho Chehab help="files to process – if supplied the --full flag is ignored") 405*210a923aSMauro Carvalho Chehab 406*210a923aSMauro Carvalho Chehab parser.add_argument("--full", "-f", action="store_true", 407*210a923aSMauro Carvalho Chehab help="Force a full scan of Documentation/*") 408*210a923aSMauro Carvalho Chehab 409*210a923aSMauro Carvalho Chehab parser.add_argument("--regression", "-r", action="store_true", 410*210a923aSMauro Carvalho Chehab help="Use YAML format to check for regressions") 411*210a923aSMauro Carvalho Chehab 412*210a923aSMauro Carvalho Chehab parser.add_argument("--work-dir", "-w", default=WORK_DIR, 413*210a923aSMauro Carvalho Chehab help="work dir (default: %(default)s)") 414*210a923aSMauro Carvalho Chehab 415*210a923aSMauro Carvalho Chehab parser.add_argument("--clean", "-c", action="store_true", 416*210a923aSMauro Carvalho Chehab help="Clean caches") 417*210a923aSMauro Carvalho Chehab 418*210a923aSMauro Carvalho Chehab args = parser.parse_args() 419*210a923aSMauro Carvalho Chehab 420*210a923aSMauro Carvalho Chehab if args.files and args.full: 421*210a923aSMauro Carvalho Chehab raise argparse.ArgumentError(args.full, 422*210a923aSMauro Carvalho Chehab "cannot combine '--full' with an explicit file list") 423*210a923aSMauro Carvalho Chehab 424*210a923aSMauro Carvalho Chehab work_dir = os.path.abspath(args.work_dir) 425*210a923aSMauro Carvalho Chehab 426*210a923aSMauro Carvalho Chehab # Initialize cache 427*210a923aSMauro Carvalho Chehab cache = CacheManager(work_dir) 428*210a923aSMauro Carvalho Chehab cache.initialize() 429*210a923aSMauro Carvalho Chehab 430*210a923aSMauro Carvalho Chehab # Validate git repository 431*210a923aSMauro Carvalho Chehab git_helper = GitHelper(work_dir) 432*210a923aSMauro Carvalho Chehab if not git_helper.is_inside_repository(): 433*210a923aSMauro Carvalho Chehab raise RuntimeError("Must run inside Git repository") 434*210a923aSMauro Carvalho Chehab 435*210a923aSMauro Carvalho Chehab old_commit, new_commit = args.commits 436*210a923aSMauro Carvalho Chehab 437*210a923aSMauro Carvalho Chehab old_commit = git_helper.get_short_hash(old_commit) 438*210a923aSMauro Carvalho Chehab new_commit = git_helper.get_short_hash(new_commit) 439*210a923aSMauro Carvalho Chehab 440*210a923aSMauro Carvalho Chehab # Validate commits 441*210a923aSMauro Carvalho Chehab for commit in [old_commit, new_commit]: 442*210a923aSMauro Carvalho Chehab if not git_helper.is_valid_commit(commit): 443*210a923aSMauro Carvalho Chehab raise RuntimeError(f"Commit '{commit}' does not exist") 444*210a923aSMauro Carvalho Chehab 445*210a923aSMauro Carvalho Chehab # Check for uncommitted changes 446*210a923aSMauro Carvalho Chehab if git_helper.has_uncommitted_changes(): 447*210a923aSMauro Carvalho Chehab raise RuntimeError("Uncommitted changes present. Commit or stash first.") 448*210a923aSMauro Carvalho Chehab 449*210a923aSMauro Carvalho Chehab runner = KernelDocRunner(git_helper.work_dir, KDOC_BINARY) 450*210a923aSMauro Carvalho Chehab 451*210a923aSMauro Carvalho Chehab # Get files to be parsed 452*210a923aSMauro Carvalho Chehab cache_msg = " (results will be cached)" 453*210a923aSMauro Carvalho Chehab if args.full: 454*210a923aSMauro Carvalho Chehab kdoc_files = ["."] 455*210a923aSMauro Carvalho Chehab diff_type = "full" 456*210a923aSMauro Carvalho Chehab print(f"Parsing all files at {work_dir}") 457*210a923aSMauro Carvalho Chehab if not args.files: 458*210a923aSMauro Carvalho Chehab diff_type = "partial" 459*210a923aSMauro Carvalho Chehab kdoc_files = runner.find_kdoc_references() 460*210a923aSMauro Carvalho Chehab print(f"Parsing files with kernel-doc markups at {work_dir}/Documentation") 461*210a923aSMauro Carvalho Chehab else: 462*210a923aSMauro Carvalho Chehab diff_type = "no-cache" 463*210a923aSMauro Carvalho Chehab cache_msg = "" 464*210a923aSMauro Carvalho Chehab kdoc_files = args.files 465*210a923aSMauro Carvalho Chehab 466*210a923aSMauro Carvalho Chehab tmp_dir = DIR_NAME["tmp"] 467*210a923aSMauro Carvalho Chehab out_path = DIR_NAME[diff_type] 468*210a923aSMauro Carvalho Chehab 469*210a923aSMauro Carvalho Chehab if not args.regression: 470*210a923aSMauro Carvalho Chehab print(f"Output will be stored at: {out_path}{cache_msg}") 471*210a923aSMauro Carvalho Chehab 472*210a923aSMauro Carvalho Chehab # Just in case - should never happen in practice 473*210a923aSMauro Carvalho Chehab if not kdoc_files: 474*210a923aSMauro Carvalho Chehab raise argparse.ArgumentError(args.files, 475*210a923aSMauro Carvalho Chehab "No kernel-doc references found") 476*210a923aSMauro Carvalho Chehab 477*210a923aSMauro Carvalho Chehab original_head = git_helper.get_current_branch() 478*210a923aSMauro Carvalho Chehab 479*210a923aSMauro Carvalho Chehab old_cache = cache.get_commit_cache(old_commit, out_path) 480*210a923aSMauro Carvalho Chehab new_cache = cache.get_commit_cache(new_commit, out_path) 481*210a923aSMauro Carvalho Chehab 482*210a923aSMauro Carvalho Chehab with SignalHandler(git_helper, original_head): 483*210a923aSMauro Carvalho Chehab if args.clean or diff_type == "no-cache": 484*210a923aSMauro Carvalho Chehab for cache_dir in [old_cache, new_cache]: 485*210a923aSMauro Carvalho Chehab if cache_dir and os.path.exists(cache_dir): 486*210a923aSMauro Carvalho Chehab shutil.rmtree(cache_dir) 487*210a923aSMauro Carvalho Chehab 488*210a923aSMauro Carvalho Chehab if args.regression or not os.path.exists(old_cache): 489*210a923aSMauro Carvalho Chehab old_success = runner.run(old_commit, tmp_dir, old_cache, kdoc_files, 490*210a923aSMauro Carvalho Chehab args.regression, False) 491*210a923aSMauro Carvalho Chehab else: 492*210a923aSMauro Carvalho Chehab old_success = True 493*210a923aSMauro Carvalho Chehab 494*210a923aSMauro Carvalho Chehab if args.regression or not os.path.exists(new_cache): 495*210a923aSMauro Carvalho Chehab new_success = runner.run(new_commit, tmp_dir, new_cache, kdoc_files, 496*210a923aSMauro Carvalho Chehab args.regression, True) 497*210a923aSMauro Carvalho Chehab else: 498*210a923aSMauro Carvalho Chehab new_success = True 499*210a923aSMauro Carvalho Chehab 500*210a923aSMauro Carvalho Chehab if not (old_success and new_success): 501*210a923aSMauro Carvalho Chehab raise RuntimeError("Failed to generate documentation") 502*210a923aSMauro Carvalho Chehab 503*210a923aSMauro Carvalho Chehab if not args.regression: 504*210a923aSMauro Carvalho Chehab diff_manager = DiffManager() 505*210a923aSMauro Carvalho Chehab diff_manager.diff_directories(old_cache, new_cache) 506*210a923aSMauro Carvalho Chehab 507*210a923aSMauro Carvalho Chehabif __name__ == "__main__": 508*210a923aSMauro Carvalho Chehab main() 509