#!/usr/bin/env python3 import argparse import fileinput import io import os import shutil import subprocess GIT = shutil.which("git") def update_file(file, lineno, old, new): """ Replace all occurrences of the old substring by the new substring. :param file: The file to update. :param lineno: The line number to update. :param old: The old substring. :param new: The new substring. """ print("* Updating file in place") with fileinput.FileInput(file, inplace=True) as f: for line in f: if f.lineno() == lineno and old in line: print(line.replace(old, new), end="") else: print(line, end="") def analyze_file(file, lineno, commits_and_tags, dry_run=False): """ Analyze the given file. :param file: The file to analyze. :param lineno: The line number to analyze. :param commits_and_tags: The output dictionary mapping commits to release tags. :param dry_run: Whether or not this is a dry run. """ print(f"Analyzing {file}:{lineno}") line_sha = ( subprocess.check_output( [GIT, "blame", "--porcelain", "-L", f"{lineno},{lineno}", file], text=True ) .split("\n", 1)[0] .split(" ", 1)[0] ) print(f"* first sha: {line_sha}") first_tag = subprocess.check_output( [GIT, "tag", "--sort=creatordate", "--contains", line_sha, "jenkins-*"], text=True, ).split("\n", 1)[0] if first_tag: print(f"* first tag was {first_tag}") commits_and_tags[line_sha] = first_tag if not dry_run: since_version = first_tag.replace("jenkins-", "") update_file( file, int(lineno), "@since TODO", f"@since {since_version}", ) update_file( file, int(lineno), '@Deprecated(since = "TODO")', f'@Deprecated(since = "{since_version}")', ) update_file( file, int(lineno), '@RestrictedSince("TODO")', f'@RestrictedSince("{since_version}")', ) else: print( "* Not updating file, no tag found. " "Normal if the associated PR/commit is not merged and released yet; " "otherwise make sure to fetch tags from jenkinsci/jenkins" ) print() # Add a newline for markdown rendering def analyze_files(commits_and_tags, dry_run=False): """ Analyze all files in the repository. :param commits_and_tags: The output dictionary mapping commits to release tags. :param dry_run: Whether or not this is a dry run. """ cmd = [ GIT, "grep", "--line-number", "-E", '@since TODO|@Deprecated\\(since = "TODO"\\)|@RestrictedSince\\("TODO"\\)', "--", "*.java", "*.jelly", "*.js", ] is_ci = "CI" in os.environ if is_ci: print("
Detailed output\n\n") with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: for line in io.TextIOWrapper(proc.stdout): parts = line.rstrip().split(":", 2) analyze_file(parts[0], parts[1], commits_and_tags, dry_run=dry_run) retcode = proc.wait() if retcode: raise subprocess.CalledProcessError(retcode, cmd) print() if is_ci: print("
\n") def display_results(commits_and_tags): """ Display the results of the analysis. :param commits_and_tags: The output dictionary mapping commits to release tags. """ print("List of commits introducing new API and the first release they went in:") releases = {release for release in commits_and_tags.values()} for release in sorted(releases): print(f"* https://github.com/jenkinsci/jenkins/releases/tag/{release}") for commit, first_release in commits_and_tags.items(): if release == first_release: print(f" - https://github.com/jenkinsci/jenkins/commit/{commit}") def main(): """ Update '@since TODO', '@Deprecated(since = "TODO")', and '@RestrictedSince("TODO")' entries with actual Jenkins release versions. This script is a developer tool, to be used by maintainers. """ parser = argparse.ArgumentParser( description="Update '@since TODO', '@Deprecated(since = \"TODO\")', and '@RestrictedSince(\"TODO\")' entries " "with actual Jenkins release versions. " ) parser.add_argument("-n", "--dry-run", help="Dry run", action="store_true") args = parser.parse_args() commits_and_tags = {} analyze_files(commits_and_tags, dry_run=args.dry_run) if commits_and_tags: display_results(commits_and_tags) if __name__ == "__main__": main()