Skip to content

Commit

Permalink
Various improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane committed Feb 25, 2025
1 parent 9533c98 commit 0b585e5
Showing 1 changed file with 147 additions and 42 deletions.
189 changes: 147 additions & 42 deletions tools/make_backports.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def run(
return result


def fetch_needs_backport_pr_numbers() -> tuple[int, ...]:
def fetch_needs_backport_pr_numbers(args) -> tuple[int, ...]:
"""Use gh cli to collect the set of PRs that are labeled as needs_backport.
Then cache them to disk. This is the implementation for --fetch-backport-prs.
Expand All @@ -51,6 +51,10 @@ def get_needs_backport_pr_numbers() -> tuple[int, ...]:
return tuple(int(line) for line in lines)


#
# Commit log parsing
#

# we use history_idx to sort by age.
CommitInfo = namedtuple(
"CommitInfo", ["pr_number", "shorthash", "shortlog", "history_idx"]
Expand All @@ -62,9 +66,13 @@ class CommitHistory:

commits: dict[int, CommitInfo]

def __init__(self):
@classmethod
def from_git(self):
result = run(["git", "log", "--oneline", "main"], capture_output=True)
lines = result.stdout.splitlines()
return CommitHistory(lines)

def __init__(self, lines):
commits = {}
PR_NUMBER_RE = re.compile(r"\(#[0-9]+\)$")
for history_idx, line in enumerate(lines):
Expand All @@ -84,12 +92,14 @@ def lookup_pr(self, pr_number: int) -> CommitInfo:
def get_commits() -> list[CommitInfo]:
"""Return the CommitInfo of the PRs we want to backport"""
pr_numbers = get_needs_backport_pr_numbers()
commit_history = CommitHistory()
commit_history = CommitHistory.from_git()
commits = [commit_history.lookup_pr(x) for x in pr_numbers]
return sorted(commits, key=lambda c: -c.history_idx)


#
# Changelog parsing
#


@dataclass
Expand Down Expand Up @@ -342,16 +352,50 @@ def remove_release_notes_from_unreleased_section(
self.unreleased.delete_entry(idx)


def show_missing_changelogs() -> None:
#
# Main commands
#


def add_backport_pr(args):
pr_number_str = args.pr_number
run(
[
"gh",
"pr",
"edit",
pr_number_str.removeprefix("#"),
"--add-label",
"needs backport",
]
)
fetch_needs_backport_pr_numbers(None)


def remove_needs_backport_labels(args) -> None:
for pr_number in get_needs_backport_pr_numbers():
run(["gh", "pr", "edit", str(pr_number), "--remove-label", "needs backport"])


def show_missing_changelogs(args) -> None:
changelog = Changelog(CHANGELOG).parse()
changelog.unreleased.create_pr_index()
commits = get_commits()
for commit in commits:
pr_number = commit.pr_number
if pr_number not in changelog.unreleased.pr_index:
print(pr_number, commit.shortlog)
missing_changelogs = [
commit
for commit in commits
if commit.pr_number not in changelog.unreleased.pr_index
]
for commit in missing_changelogs:
if args.web:
run(["gh", "pr", "view", "-w", str(commit.pr_number)])
else:
print(commit.pr_number, commit.shorthash, commit.shortlog)


def make_changelog_branch(version: str) -> None:
def make_changelog_branch(args) -> None:
version = args.new_version
run(["git", "fetch", "upstream", "main:main"])
changelog = Changelog(CHANGELOG).parse()
changelog.unreleased.create_pr_index()
run(["git", "switch", "main"])
Expand All @@ -364,71 +408,132 @@ def make_changelog_branch(version: str) -> None:
run(["git", "commit", "-m", f"Update changelog for v{version}"])


def make_backport_branch(version: str) -> None:
def make_backport_branch(args) -> None:
version = args.new_version
run(["git", "fetch", "upstream", "main:main"])
run(["git", "fetch", "upstream", "stable:stable"])
changelog = Changelog(CHANGELOG).parse()
changelog.unreleased.create_pr_index()
run(["git", "switch", "stable"])
run(["git", "submodule", "update"])
run(["git", "switch", "-C", f"backports-for-{version}-tmp"])
commits = get_commits()
for n, cur_commit in enumerate(commits):
result = run(["git", "cherry-pick", cur_commit.shorthash], check=False)
result = run(
["git", "-c", "core.editor=true", "cherry-pick", cur_commit.shorthash],
check=False,
capture_output=True,
)
for line in result.stdout.splitlines():
# We need to resolve submodule conflicts ourselves. We always pick
# the submodule version from the commit we are cherry-picking.
if not line.startswith("CONFLICT (submodule)"):
continue
path = line.partition("Merge conflict in ")[-1]
run(["git", "checkout", cur_commit.shorthash, "--", path])
changelog.set_patch_release_notes(version, commits[: n + 1])
changelog.write_text(include_unreleased=False)
run(["git", "add", "docs/project/changelog.md"])
if result.returncode == 0:
print("cherry-pick succeeded first try", cur_commit.shortlog)
run(["git", "commit", "--amend"])
else:
run(["git", "cherry-pick", "--continue"])
print("cherry-pick attempting continue", cur_commit.shortlog)
run(["git", "cherry-pick", "--continue", "--no-edit"])

commits = get_commits()


def remove_needs_backport_labels() -> None:
for pr_number in get_needs_backport_pr_numbers():
run(["gh", "pr", "edit", str(pr_number), "--remove-label", "needs backport"])
def open_release_prs(args):
version = args.new_version
INSERT_ACTUAL_DATE = "- [ ] Insert the actual date in the changelog\n"
MERGE_DONT_SQUASH = "- [] Merge, don't squash"
BACKPORTS_BRANCH = f"backports-for-{version}-tmp"
CHANGELOG_BRANCH = f"changelog-for-{version}-tmp"

run(["git", "switch", BACKPORTS_BRANCH])
run(
[
"gh",
"pr",
"create",
"--base",
"stable",
"--title",
f"Backports for v{version}",
"--body",
INSERT_ACTUAL_DATE + MERGE_DONT_SQUASH,
"--web",
]
)

run(["git", "switch", CHANGELOG_BRANCH])
run(
[
"gh",
"pr",
"create",
"--base",
"main",
"--title",
f"Changelog for v{version}",
"--body",
INSERT_ACTUAL_DATE,
"--web",
]
)


def parse_args():
parser = argparse.ArgumentParser("Apply backports")
parser.add_argument("new_version")
parser.add_argument(
"--fetch-backport-prs",
action="store_true",
parser.set_defaults(func=lambda args: parser.print_help())
subparsers = parser.add_subparsers()

add_backport_parser = subparsers.add_parser(
"add-backport-pr", help="Add the needs-backport label to a PR"
)
add_backport_parser.add_argument("pr_number")
add_backport_parser.set_defaults(func=add_backport_pr)

fetch_backports_parser = subparsers.add_parser(
"fetch-backports",
help="Fetch the list of PRs with the 'needs backport' label and cache to disk. Must be run first.",
)
parser.add_argument(
"--missing-changelogs",
action="store_true",
fetch_backports_parser.set_defaults(func=fetch_needs_backport_pr_numbers)

missing_changelogs_parser = subparsers.add_parser(
"missing-changelogs",
help="List the PRs labeled as 'needs backport' that don't have a changelog",
)
parser.add_argument(
"--changelog-branch",
action="store_true",
help="Make changelog-for-version branch",
missing_changelogs_parser.add_argument(
"-w", "--web", action="store_true", help="Open missing changelog prs in browser"
)
missing_changelogs_parser.set_defaults(func=show_missing_changelogs)

changelog_branch_parse = subparsers.add_parser(
"changelog-branch", help="Make changelog-for-version branch"
)
parser.add_argument(
"--backport-branch",
action="store_true",
help="Make backports-for-version branch",
changelog_branch_parse.add_argument("new_version")
changelog_branch_parse.set_defaults(func=make_changelog_branch)

backport_branch_parse = subparsers.add_parser(
"backport-branch", help="Make backports-for-version branch"
)
backport_branch_parse.add_argument("new_version")
backport_branch_parse.set_defaults(func=make_backport_branch)

open_release_prs_parse = subparsers.add_parser(
"open-release-prs", help="Open PRs for the backports and changelog branches"
)
open_release_prs_parse.add_argument("new_version")
open_release_prs_parse.set_defaults(func=open_release_prs)

return parser.parse_args()


def main():
args = parse_args()
if args.fetch_backport_prs:
fetch_needs_backport_pr_numbers()
return
if args.missing_changelogs:
show_missing_changelogs()
return
if args.changelog_branch:
make_changelog_branch(args.new_version)
return
if args.backport_branch:
make_backport_branch(args.new_version)
return
args.func(args)


if __name__ == "__main__":
Expand Down

0 comments on commit 0b585e5

Please sign in to comment.