#!/usr/bin/env python3 """ This script inserts "versionadded" directive into every .rst document and every .cmake module with .rst documentation comment. """ import re import pathlib import subprocess import argparse tag_re = re.compile(r'^v3\.(\d+)\.(\d+)(?:-rc(\d+))?$') path_re = re.compile(r'Help/(?!dev|guide|manual|cpack_|release).*\.rst|Modules/[^/]*\.cmake$') def git_root(): result = subprocess.run( ['git', 'rev-parse', '--show-toplevel'], check=True, universal_newlines=True, capture_output=True) return pathlib.Path(result.stdout.strip()) def git_tags(): result = subprocess.run(['git', 'tag'], check=True, universal_newlines=True, capture_output=True) return [tag for tag in result.stdout.splitlines() if tag_re.match(tag)] def git_list_tree(ref): result = subprocess.run( ['git', 'ls-tree', '-r', '--full-name', '--name-only', ref, ':/'], check=True, universal_newlines=True, capture_output=True) return [path for path in result.stdout.splitlines() if path_re.match(path)] def tag_version(tag): return re.sub(r'^v|\.0(-rc\d+)?$', '', tag) def tag_sortkey(tag): return tuple(int(part or '1000') for part in tag_re.match(tag).groups()) def make_version_map(baseline, since, next_version): versions = {} if next_version: for path in git_list_tree('HEAD'): versions[path] = next_version for tag in sorted(git_tags(), key=tag_sortkey, reverse=True): version = tag_version(tag) for path in git_list_tree(tag): versions[path] = version if baseline: for path in git_list_tree(baseline): versions[path] = None if since: for path in git_list_tree(since): versions.pop(path, None) return versions cmake_version_re = re.compile( rb'set\(CMake_VERSION_MAJOR\s+(\d+)\)\s+set\(CMake_VERSION_MINOR\s+(\d+)\)\s+set\(CMake_VERSION_PATCH\s+(\d+)\)', re.S) def cmake_version(path): match = cmake_version_re.search(path.read_bytes()) major, minor, patch = map(int, match.groups()) minor += patch > 20000000 return f'{major}.{minor}' stamp_re = re.compile( rb'(?P(^|\[\.rst:\r?\n)[^\r\n]+\r?\n[*^\-=#]+(?P\r?\n))(?P\s*\.\. versionadded::[^\r\n]*\r?\n)?') stamp_pattern_add = rb'\g\g.. versionadded:: VERSION\g' stamp_pattern_remove = rb'\g' def update_file(path, version, overwrite): try: data = path.read_bytes() except FileNotFoundError as e: return False def _replacement(match): if not overwrite and match.start('STAMP') != -1: return match.group() if version: pattern = stamp_pattern_add.replace(b'VERSION', version.encode('utf-8')) else: pattern = stamp_pattern_remove return match.expand(pattern) new_data, nrepl = stamp_re.subn(_replacement, data, 1) if nrepl and new_data != data: path.write_bytes(new_data) return True return False def update_repo(repo_root, version_map, overwrite): total = 0 for path, version in version_map.items(): if update_file(repo_root / path, version, overwrite): print(f"Version {version or '':6} for {path}") total += 1 print(f"Updated {total} file(s)") def main(): parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument('--overwrite', action='store_true', help="overwrite existing version tags") parser.add_argument('--baseline', metavar='TAG', default='v3.0.0', help="files present in this tag won't be stamped (default: v3.0.0)") parser.add_argument('--since', metavar='TAG', help="apply changes only to files added after this tag") parser.add_argument('--next-version', metavar='VER', help="version for files not present in any tag (default: from CMakeVersion.cmake)") args = parser.parse_args() try: repo_root = git_root() next_version = args.next_version or cmake_version(repo_root / 'Source/CMakeVersion.cmake') version_map = make_version_map(args.baseline, args.since, next_version) update_repo(repo_root, version_map, args.overwrite) except subprocess.CalledProcessError as e: print(f"Command '{' '.join(e.cmd)}' returned code {e.returncode}:\n{e.stderr.strip()}") if __name__ == '__main__': main()