update_versions.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env python3
  2. """
  3. This script inserts "versionadded" directive into every .rst document
  4. and every .cmake module with .rst documentation comment.
  5. """
  6. import re
  7. import pathlib
  8. import subprocess
  9. import argparse
  10. tag_re = re.compile(r'^v3\.(\d+)\.(\d+)(?:-rc(\d+))?$')
  11. path_re = re.compile(r'Help/(?!dev|guide|manual|cpack_|release).*\.rst|Modules/[^/]*\.cmake$')
  12. def git_root():
  13. result = subprocess.run(
  14. ['git', 'rev-parse', '--show-toplevel'], check=True, universal_newlines=True, capture_output=True)
  15. return pathlib.Path(result.stdout.strip())
  16. def git_tags():
  17. result = subprocess.run(['git', 'tag'], check=True, universal_newlines=True, capture_output=True)
  18. return [tag for tag in result.stdout.splitlines() if tag_re.match(tag)]
  19. def git_list_tree(ref):
  20. result = subprocess.run(
  21. ['git', 'ls-tree', '-r', '--full-name', '--name-only', ref, ':/'],
  22. check=True, universal_newlines=True, capture_output=True)
  23. return [path for path in result.stdout.splitlines() if path_re.match(path)]
  24. def tag_version(tag):
  25. return re.sub(r'^v|\.0(-rc\d+)?$', '', tag)
  26. def tag_sortkey(tag):
  27. return tuple(int(part or '1000') for part in tag_re.match(tag).groups())
  28. def make_version_map(baseline, since, next_version):
  29. versions = {}
  30. if next_version:
  31. for path in git_list_tree('HEAD'):
  32. versions[path] = next_version
  33. for tag in sorted(git_tags(), key=tag_sortkey, reverse=True):
  34. version = tag_version(tag)
  35. for path in git_list_tree(tag):
  36. versions[path] = version
  37. if baseline:
  38. for path in git_list_tree(baseline):
  39. versions[path] = None
  40. if since:
  41. for path in git_list_tree(since):
  42. versions.pop(path, None)
  43. return versions
  44. cmake_version_re = re.compile(
  45. rb'set\(CMake_VERSION_MAJOR\s+(\d+)\)\s+set\(CMake_VERSION_MINOR\s+(\d+)\)\s+set\(CMake_VERSION_PATCH\s+(\d+)\)', re.S)
  46. def cmake_version(path):
  47. match = cmake_version_re.search(path.read_bytes())
  48. major, minor, patch = map(int, match.groups())
  49. minor += patch > 20000000
  50. return f'{major}.{minor}'
  51. stamp_re = re.compile(
  52. rb'(?P<PREFIX>(^|\[\.rst:\r?\n)[^\r\n]+\r?\n[*^\-=#]+(?P<NL>\r?\n))(?P<STAMP>\s*\.\. versionadded::[^\r\n]*\r?\n)?')
  53. stamp_pattern_add = rb'\g<PREFIX>\g<NL>.. versionadded:: VERSION\g<NL>'
  54. stamp_pattern_remove = rb'\g<PREFIX>'
  55. def update_file(path, version, overwrite):
  56. try:
  57. data = path.read_bytes()
  58. except FileNotFoundError as e:
  59. return False
  60. def _replacement(match):
  61. if not overwrite and match.start('STAMP') != -1:
  62. return match.group()
  63. if version:
  64. pattern = stamp_pattern_add.replace(b'VERSION', version.encode('utf-8'))
  65. else:
  66. pattern = stamp_pattern_remove
  67. return match.expand(pattern)
  68. new_data, nrepl = stamp_re.subn(_replacement, data, 1)
  69. if nrepl and new_data != data:
  70. path.write_bytes(new_data)
  71. return True
  72. return False
  73. def update_repo(repo_root, version_map, overwrite):
  74. total = 0
  75. for path, version in version_map.items():
  76. if update_file(repo_root / path, version, overwrite):
  77. print(f"Version {version or '<none>':6} for {path}")
  78. total += 1
  79. print(f"Updated {total} file(s)")
  80. def main():
  81. parser = argparse.ArgumentParser(allow_abbrev=False)
  82. parser.add_argument('--overwrite', action='store_true', help="overwrite existing version tags")
  83. parser.add_argument('--baseline', metavar='TAG', default='v3.0.0',
  84. help="files present in this tag won't be stamped (default: v3.0.0)")
  85. parser.add_argument('--since', metavar='TAG',
  86. help="apply changes only to files added after this tag")
  87. parser.add_argument('--next-version', metavar='VER',
  88. help="version for files not present in any tag (default: from CMakeVersion.cmake)")
  89. args = parser.parse_args()
  90. try:
  91. repo_root = git_root()
  92. next_version = args.next_version or cmake_version(repo_root / 'Source/CMakeVersion.cmake')
  93. version_map = make_version_map(args.baseline, args.since, next_version)
  94. update_repo(repo_root, version_map, args.overwrite)
  95. except subprocess.CalledProcessError as e:
  96. print(f"Command '{' '.join(e.cmd)}' returned code {e.returncode}:\n{e.stderr.strip()}")
  97. if __name__ == '__main__':
  98. main()