xref: /linux/tools/docs/kdoc_diff (revision 5181afcdf99527dd92a88f80fc4d0d8013e1b510)
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