update-since-todo.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. #!/usr/bin/env python3
  2. import argparse
  3. import fileinput
  4. import io
  5. import os
  6. import shutil
  7. import subprocess
  8. GIT = shutil.which("git")
  9. def update_file(file, lineno, old, new):
  10. """
  11. Replace all occurrences of the old substring by the new substring.
  12. :param file: The file to update.
  13. :param lineno: The line number to update.
  14. :param old: The old substring.
  15. :param new: The new substring.
  16. """
  17. print("* Updating file in place")
  18. with fileinput.FileInput(file, inplace=True) as f:
  19. for line in f:
  20. if f.lineno() == lineno and old in line:
  21. print(line.replace(old, new), end="")
  22. else:
  23. print(line, end="")
  24. def analyze_file(file, lineno, commits_and_tags, dry_run=False):
  25. """
  26. Analyze the given file.
  27. :param file: The file to analyze.
  28. :param lineno: The line number to analyze.
  29. :param commits_and_tags: The output dictionary mapping commits to release tags.
  30. :param dry_run: Whether or not this is a dry run.
  31. """
  32. print(f"Analyzing {file}:{lineno}")
  33. line_sha = (
  34. subprocess.check_output(
  35. [GIT, "blame", "--porcelain", "-L", f"{lineno},{lineno}", file], text=True
  36. )
  37. .split("\n", 1)[0]
  38. .split(" ", 1)[0]
  39. )
  40. print(f"* first sha: {line_sha}")
  41. first_tag = subprocess.check_output(
  42. [GIT, "tag", "--sort=creatordate", "--contains", line_sha, "jenkins-*"],
  43. text=True,
  44. ).split("\n", 1)[0]
  45. if first_tag:
  46. print(f"* first tag was {first_tag}")
  47. commits_and_tags[line_sha] = first_tag
  48. if not dry_run:
  49. since_version = first_tag.replace("jenkins-", "")
  50. update_file(
  51. file,
  52. int(lineno),
  53. "@since TODO",
  54. f"@since {since_version}",
  55. )
  56. update_file(
  57. file,
  58. int(lineno),
  59. '@Deprecated(since = "TODO")',
  60. f'@Deprecated(since = "{since_version}")',
  61. )
  62. update_file(
  63. file,
  64. int(lineno),
  65. '@RestrictedSince("TODO")',
  66. f'@RestrictedSince("{since_version}")',
  67. )
  68. else:
  69. print(
  70. "* Not updating file, no tag found. "
  71. "Normal if the associated PR/commit is not merged and released yet; "
  72. "otherwise make sure to fetch tags from jenkinsci/jenkins"
  73. )
  74. print() # Add a newline for markdown rendering
  75. def analyze_files(commits_and_tags, dry_run=False):
  76. """
  77. Analyze all files in the repository.
  78. :param commits_and_tags: The output dictionary mapping commits to release tags.
  79. :param dry_run: Whether or not this is a dry run.
  80. """
  81. cmd = [
  82. GIT,
  83. "grep",
  84. "--line-number",
  85. "-E",
  86. '@since TODO|@Deprecated\\(since = "TODO"\\)|@RestrictedSince\\("TODO"\\)',
  87. "--",
  88. "*.java",
  89. "*.jelly",
  90. "*.js",
  91. ]
  92. is_ci = "CI" in os.environ
  93. if is_ci:
  94. print("<details><summary>Detailed output</summary>\n\n")
  95. processed = 0
  96. with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
  97. for line in io.TextIOWrapper(proc.stdout):
  98. parts = line.rstrip().split(":", 2)
  99. analyze_file(parts[0], parts[1], commits_and_tags, dry_run=dry_run)
  100. processed += 1
  101. retcode = proc.wait()
  102. if retcode not in (0, 1):
  103. raise subprocess.CalledProcessError(retcode, cmd)
  104. if processed == 0:
  105. print("No '@since TODO', '@Deprecated(since = \"TODO\")', or '@RestrictedSince(\"TODO\")' tags found.")
  106. if is_ci:
  107. print("</details>\n")
  108. def display_results(commits_and_tags):
  109. """
  110. Display the results of the analysis.
  111. :param commits_and_tags: The output dictionary mapping commits to release tags.
  112. """
  113. print("List of commits introducing new API and the first release they went in:")
  114. releases = {release for release in commits_and_tags.values()}
  115. for release in sorted(releases):
  116. print(f"* https://github.com/jenkinsci/jenkins/releases/tag/{release}")
  117. for commit, first_release in commits_and_tags.items():
  118. if release == first_release:
  119. print(f" - https://github.com/jenkinsci/jenkins/commit/{commit}")
  120. def main():
  121. """
  122. Update '@since TODO', '@Deprecated(since = "TODO")', and '@RestrictedSince("TODO")' entries
  123. with actual Jenkins release versions.
  124. This script is a developer tool, to be used by maintainers.
  125. """
  126. parser = argparse.ArgumentParser(
  127. description="Update '@since TODO', '@Deprecated(since = \"TODO\")', and '@RestrictedSince(\"TODO\")' entries "
  128. "with actual Jenkins release versions. "
  129. )
  130. parser.add_argument("-n", "--dry-run", help="Dry run", action="store_true")
  131. args = parser.parse_args()
  132. commits_and_tags = {}
  133. analyze_files(commits_and_tags, dry_run=args.dry_run)
  134. if commits_and_tags:
  135. display_results(commits_and_tags)
  136. if __name__ == "__main__":
  137. main()