Something went wrong. Try again.
(I'm so sorry!) Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509#!/usr/bin/env python3# SPDX-License-Identifier: GPL-2.0# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>.## pylint: disable=R0903,R0912,R0913,R0914,R0915,R0917
"""docdiff - Check differences between kernel‑doc output between two differentcommits.
Examples--------
Compare the kernel‑doc output between the last two 5.15 releases::
$ kdoc_diff v6.18..v6.19
Both outputs are cached
Force a complete documentation scan and clean any previous cache from6.19 to the current HEAD::
$ kdoc_diff 6.19.. --full --clean
Check differences only on a single driver since origin/main::
$ kdoc_diff origin/main drivers/media
Generate an YAML file and use it to check for regressions::
$ kdoc_diff HEAD~ drivers/media --regression
"""
import osimport sysimport argparseimport subprocessimport shutilimport reimport signal
from glob import iglob
SRC_DIR = os.path.dirname(os.path.realpath(__file__))WORK_DIR = os.path.abspath(os.path.join(SRC_DIR, "../.."))
KDOC_BINARY = os.path.join(SRC_DIR, "kernel-doc")KDOC_PARSER_TEST = os.path.join(WORK_DIR, "tools/unittests/test_kdoc_parser.py")
CACHE_DIR = ".doc_diff_cache"YAML_NAME = "out.yaml"
DIR_NAME = { "full": os.path.join(CACHE_DIR, "full"), "partial": os.path.join(CACHE_DIR, "partial"), "no-cache": os.path.join(CACHE_DIR, "no_cache"), "tmp": os.path.join(CACHE_DIR, "__tmp__"),}
class GitHelper: """Handles all Git operations"""
def __init__(self, work_dir=None): self.work_dir = work_dir
def is_inside_repository(self): """Check if we're inside a Git repository""" try: output = subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"], cwd=self.work_dir, stderr=subprocess.STDOUT, universal_newlines=True)
return output.strip() == "true" except subprocess.CalledProcessError: return False
def is_valid_commit(self, commit_hash): """ Validate that a ref (branch, tag, commit hash, etc.) can be resolved to a commit. """ try: subprocess.check_output(["git", "rev-parse", commit_hash], cwd=self.work_dir, stderr=subprocess.STDOUT) return True except subprocess.CalledProcessError: return False
def get_short_hash(self, commit_hash): """Get short commit hash""" try: return subprocess.check_output(["git", "rev-parse", "--short", commit_hash], cwd=self.work_dir, stderr=subprocess.STDOUT, universal_newlines=True).strip() except subprocess.CalledProcessError: return ""
def has_uncommitted_changes(self): """Check for uncommitted changes""" try: subprocess.check_output(["git", "diff-index", "--quiet", "HEAD", "--"], cwd=self.work_dir, stderr=subprocess.STDOUT) return False except subprocess.CalledProcessError: return True
def get_current_branch(self): """Get current branch name""" return subprocess.check_output(["git", "branch", "--show-current"], cwd=self.work_dir, universal_newlines=True).strip()
def checkout_commit(self, commit_hash, quiet=True): """Checkout a commit safely""" args = ["git", "checkout", "-f"] if quiet: args.append("-q") args.append(commit_hash) try: subprocess.check_output(args, cwd=self.work_dir, stderr=subprocess.STDOUT)
# Double-check if branch actually switched branch = self.get_short_hash("HEAD") if commit_hash != branch: raise RuntimeError(f"Branch changed to '{branch}' instead of '{commit_hash}'")
return True except subprocess.CalledProcessError as e: print(f"ERROR: Failed to checkout {commit_hash}: {e}", file=sys.stderr) return False
class CacheManager: """Manages persistent cache directories"""
def __init__(self, work_dir): self.work_dir = work_dir
def initialize(self): """Create cache directories if they don't exist""" for dir_path in DIR_NAME.values(): abs_path = os.path.join(self.work_dir, dir_path) if not os.path.exists(abs_path): os.makedirs(abs_path, exist_ok=True, mode=0o755)
def get_commit_cache(self, commit_hash, path): """Generate cache path for a commit""" hash_short = GitHelper(self.work_dir).get_short_hash(commit_hash) if not hash_short: hash_short = commit_hash
return os.path.join(path, hash_short)
class KernelDocRunner: """Runs kernel-doc documentation generator"""
def __init__(self, work_dir, kdoc_binary): self.work_dir = work_dir self.kdoc_binary = kdoc_binary self.kdoc_files = None
def find_kdoc_references(self): """Find all files marked with kernel-doc:: directives""" if self.kdoc_files: print("Using cached Kdoc refs") return self.kdoc_files
print("Finding kernel-doc entries in Documentation...")
files = os.path.join(self.work_dir, 'Documentation/**/*.rst') pattern = re.compile(r"^\.\.\s+kernel-doc::\s*(\S+)") kdoc_files = set()
for file_path in iglob(files, recursive=True): try: with open(file_path, 'r', encoding='utf-8') as fp: for line in fp: match = pattern.match(line.strip()) if match: kdoc_files.add(match.group(1))
except OSError: continue
self.kdoc_files = list(kdoc_files)
return self.kdoc_files
def gen_yaml(self, yaml_file, kdoc_files): """Runs kernel-doc to generate a yaml file with man and rst.""" cmd = [self.kdoc_binary, "--man", "--rst", "--yaml", yaml_file] cmd += kdoc_files
print(f"YAML regression test file will be stored at: {yaml_file}")
try: subprocess.check_call(cmd, cwd=self.work_dir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: return False
return True
def run_unittest(self, yaml_file): """Run unit tests with the generated yaml file""" cmd = [KDOC_PARSER_TEST, "-q", "--yaml", yaml_file] result = subprocess.run(cmd, cwd=self.work_dir)
if result.returncode: print("To check for problems, try to run it again with -v\n") print("Use -k <regex> to filter results\n\n\t$", end="") print(" ".join(cmd) + "\n")
return True
def normal_run(self, tmp_dir, output_dir, kdoc_files): """Generate man, rst and errors, storing them at tmp_dir.""" os.makedirs(tmp_dir, exist_ok=True)
try: with open(os.path.join(tmp_dir, "man.log"), "w", encoding="utf-8") as out: subprocess.check_call([self.kdoc_binary, "--man"] + kdoc_files, cwd=self.work_dir, stdout=out, stderr=subprocess.DEVNULL)
with open(os.path.join(tmp_dir, "rst.log"), "w", encoding="utf-8") as out: with open(os.path.join(tmp_dir, "err.log"), "w", encoding="utf-8") as err: subprocess.check_call([self.kdoc_binary, "--rst"] + kdoc_files, cwd=self.work_dir, stdout=out, stderr=err) except subprocess.CalledProcessError: return False
if output_dir: os.replace(tmp_dir, output_dir)
return True
def run(self, commit_hash, tmp_dir, output_dir, kdoc_files, is_regression, is_end): """Run kernel-doc on its several ways""" if not kdoc_files: raise RuntimeError("No kernel-doc references found")
git_helper = GitHelper(self.work_dir) if not git_helper.checkout_commit(commit_hash, quiet=True): raise RuntimeError(f"ERROR: can't checkout commit {commit_hash}")
print(f"Processing {commit_hash}...")
if not is_regression: return self.normal_run(tmp_dir, output_dir, kdoc_files)
yaml_file = os.path.join(tmp_dir, YAML_NAME)
if not is_end: return self.gen_yaml(yaml_file, kdoc_files)
return self.run_unittest(yaml_file)
class DiffManager: """Compare documentation output directories with an external diff.""" def __init__(self, diff_tool="diff", diff_args=None): self.diff_tool = diff_tool # default: unified, no context, ignore whitespace changes self.diff_args = diff_args or ["-u0", "-w"]
def diff_directories(self, dir1, dir2): """Compare two directories using an external diff.""" print(f"\nDiffing {dir1} and {dir2}:")
dir1_files = set() dir2_files = set() has_diff = False
for root, _, files in os.walk(dir1): for file in files: dir1_files.add(os.path.relpath(os.path.join(root, file), dir1)) for root, _, files in os.walk(dir2): for file in files: dir2_files.add(os.path.relpath(os.path.join(root, file), dir2))
common_files = sorted(dir1_files & dir2_files) for file in common_files: f1 = os.path.join(dir1, file) f2 = os.path.join(dir2, file)
cmd = [self.diff_tool] + self.diff_args + [f1, f2] try: result = subprocess.run( cmd, capture_output=True, text=True, check=False ) if result.stdout: has_diff = True print(f"\n{file}") print(result.stdout, end="") except FileNotFoundError: print(f"ERROR: {self.diff_tool} not found") sys.exit(1)
# Show files that exist only in one directory only_in_dir1 = dir1_files - dir2_files only_in_dir2 = dir2_files - dir1_files if only_in_dir1 or only_in_dir2: has_diff = True print("\nDifferential files:") for f in sorted(only_in_dir1): print(f" - {f} (only in {dir1})") for f in sorted(only_in_dir2): print(f" + {f} (only in {dir2})")
if not has_diff: print("\nNo differences between those two commits")
class SignalHandler(): """Signal handler class."""
def restore(self, force_exit=False): """Restore original HEAD state.""" if self.restored: return
print(f"Restoring original branch: {self.original_head}") try: subprocess.check_call( ["git", "checkout", "-f", self.original_head], cwd=self.git_helper.work_dir, stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as e: print(f"Failed to restore: {e}", file=sys.stderr)
for sig, handler in self.old_handler.items(): signal.signal(sig, handler)
self.restored = True
if force_exit: sys.exit(1)
def signal_handler(self, sig, _): """Handle interrupt signals.""" print(f"\nSignal {sig} received. Restoring original state...")
self.restore(force_exit=True)
def __enter__(self): """Allow using it via with command.""" for sig in [signal.SIGINT, signal.SIGTERM]: self.old_handler[sig] = signal.getsignal(sig) signal.signal(sig, self.signal_handler)
return self
def __exit__(self, *args): """Restore signals at the end of with block.""" self.restore()
def __init__(self, git_helper, original_head): self.git_helper = git_helper self.original_head = original_head self.old_handler = {} self.restored = False
def parse_commit_range(value): """Handle a commit range.""" if ".." not in value: begin = value end = "HEAD" else: begin, _, end = value.partition("..") if not end: end = "HEAD"
if not begin: raise argparse.ArgumentTypeError("Need a commit begginning")
print(f"Range: {begin} to {end}")
return begin, end
def main(): """Main code""" parser = argparse.ArgumentParser(description="Compare kernel documentation between commits") parser.add_argument("commits", type=parse_commit_range, help="commit range like old..new") parser.add_argument("files", nargs="*", help="files to process – if supplied the --full flag is ignored")
parser.add_argument("--full", "-f", action="store_true", help="Force a full scan of Documentation/*")
parser.add_argument("--regression", "-r", action="store_true", help="Use YAML format to check for regressions")
parser.add_argument("--work-dir", "-w", default=WORK_DIR, help="work dir (default: %(default)s)")
parser.add_argument("--clean", "-c", action="store_true", help="Clean caches")
args = parser.parse_args()
if args.files and args.full: raise argparse.ArgumentError(args.full, "cannot combine '--full' with an explicit file list")
work_dir = os.path.abspath(args.work_dir)
# Initialize cache cache = CacheManager(work_dir) cache.initialize()
# Validate git repository git_helper = GitHelper(work_dir) if not git_helper.is_inside_repository(): raise RuntimeError("Must run inside Git repository")
old_commit, new_commit = args.commits
old_commit = git_helper.get_short_hash(old_commit) new_commit = git_helper.get_short_hash(new_commit)
# Validate commits for commit in [old_commit, new_commit]: if not git_helper.is_valid_commit(commit): raise RuntimeError(f"Commit '{commit}' does not exist")
# Check for uncommitted changes if git_helper.has_uncommitted_changes(): raise RuntimeError("Uncommitted changes present. Commit or stash first.")
runner = KernelDocRunner(git_helper.work_dir, KDOC_BINARY)
# Get files to be parsed cache_msg = " (results will be cached)" if args.full: kdoc_files = ["."] diff_type = "full" print(f"Parsing all files at {work_dir}") if not args.files: diff_type = "partial" kdoc_files = runner.find_kdoc_references() print(f"Parsing files with kernel-doc markups at {work_dir}/Documentation") else: diff_type = "no-cache" cache_msg = "" kdoc_files = args.files
tmp_dir = DIR_NAME["tmp"] out_path = DIR_NAME[diff_type]
if not args.regression: print(f"Output will be stored at: {out_path}{cache_msg}")
# Just in case - should never happen in practice if not kdoc_files: raise argparse.ArgumentError(args.files, "No kernel-doc references found")
original_head = git_helper.get_current_branch()
old_cache = cache.get_commit_cache(old_commit, out_path) new_cache = cache.get_commit_cache(new_commit, out_path)
with SignalHandler(git_helper, original_head): if args.clean or diff_type == "no-cache": for cache_dir in [old_cache, new_cache]: if cache_dir and os.path.exists(cache_dir): shutil.rmtree(cache_dir)
if args.regression or not os.path.exists(old_cache): old_success = runner.run(old_commit, tmp_dir, old_cache, kdoc_files, args.regression, False) else: old_success = True
if args.regression or not os.path.exists(new_cache): new_success = runner.run(new_commit, tmp_dir, new_cache, kdoc_files, args.regression, True) else: new_success = True
if not (old_success and new_success): raise RuntimeError("Failed to generate documentation")
if not args.regression: diff_manager = DiffManager() diff_manager.diff_directories(old_cache, new_cache)
if __name__ == "__main__": main()