|
|
@@ -0,0 +1,115 @@
|
|
|
+#!/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<PREFIX>(^|\[\.rst:\r?\n)[^\r\n]+\r?\n[*^\-=#]+(?P<NL>\r?\n))(?P<STAMP>\s*\.\. versionadded::[^\r\n]*\r?\n)?')
|
|
|
+stamp_pattern_add = rb'\g<PREFIX>\g<NL>.. versionadded:: VERSION\g<NL>'
|
|
|
+stamp_pattern_remove = rb'\g<PREFIX>'
|
|
|
+
|
|
|
+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 '<none>':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()
|