#!/usr/bin/env python3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
import sys
from subprocess
import check_output
def exit_with_failure(message):
print(f
"ERROR: {message}", file=sys.stderr)
sys.exit(
1)
def version_string_to_underscore(version_string):
"""Convert version string like '3.118' to '3_118'."""
return version_string.replace(
".",
"_")
def version_string_to_RTM_tag(version_string):
"""Convert version string like '3.118' to 'NSS_3_118_RTM'."""
parts = version_string.split(
".")
return "NSS_" +
"_".join(parts) +
"_RTM"
def get_nspr_version(rev=
None):
"""Read the NSPR version from automation/release/nspr-version.txt.
If rev
is given, reads the file at that hg revision; otherwise reads the
working-directory copy.
"""
nspr_version_file =
"automation/release/nspr-version.txt"
if rev
is not None:
try:
content = check_output([
"hg",
"cat",
"-r", rev, nspr_version_file]).decode()
return content.splitlines()[
0].strip()
except Exception
as e:
exit_with_failure(
f
"Could not read {nspr_version_file} at revision {rev}: {e}"
)
try:
with open(nspr_version_file,
"r")
as f:
return f.readline().strip()
except FileNotFoundError:
exit_with_failure(
f
"Could not find {nspr_version_file}. Are you running from the NSS root directory?"
)
def get_rtm_tag_date(rtm_tag):
"""Return the date of the RTM tag as a formatted string, or None if tag doesn't exist."""
try:
raw = (
check_output([
"hg",
"log",
"-r", rtm_tag,
"--template",
"{date|shortdate}"])
.decode()
.strip()
)
from datetime
import datetime
dt = datetime.strptime(raw,
"%Y-%m-%d")
return dt.strftime(
"%-d %B %Y")
except Exception:
return None
def tag_exists(tag):
"""Return True if the given Mercurial tag resolves in the current repo."""
try:
check_output([
"hg",
"log",
"-r", tag,
"--template",
"x"])
return True
except Exception:
return False
def default_branch_has_version(major, minor):
"""Return True if lib/nss/nss.h on the default branch tip has NSS_VMAJOR/VMINOR set to major.minor."""
try:
content = check_output([
"hg",
"cat",
"-r",
"default",
"lib/nss/nss.h"]).decode()
return re.search(
rf
"#define\s+NSS_VMAJOR\s+{re.escape(major)}\b", content
)
and re.search(rf
"#define\s+NSS_VMINOR\s+{re.escape(minor)}\b", content)
except Exception:
return False
def node_set(revset):
"""Return the set of short hashes matching revset."""
try:
result = (
check_output([
"hg",
"log",
"-r", revset,
"--template",
"{node|short}\\n"])
.decode()
.strip()
)
except Exception:
return set()
return set(result.split())
if result
else set()
def graft_sources_on_branch(branch):
"""Return short hashes of default-branch commits grafted onto branch.
These are the source= values recorded by hg graft, truncated to
12 chars
to match the {node|short} format used elsewhere.
"""
try:
extras = check_output(
[
"hg",
"log",
"-r", f
"::'{branch}'",
"--template",
"{extras}\\n"]
).decode()
except Exception:
return set()
return {h[:
12]
for h
in re.findall(r
"(?<![a-z_])source=([0-9a-f]+)", extras)}
def find_beta_bump_commit(upper, prev_branch, major, minor):
"""Find the commit that first set lib/nss/nss.h to version {major}.{minor} Beta.
Only searches commits reachable
from upper but
not from prev_branch, so it
returns quickly when no beta bump was made.
Returns the short hash of the commit,
or None if not found.
"""
try:
cmd = [
"hg",
"log",
"-r",
f
"filelog('lib/nss/nss.h') and ::'{upper}' and not ::'{prev_branch}'",
"--template",
"{node|short}\\n",
]
result = check_output(cmd).decode().strip()
except Exception
as e:
exit_with_failure(f
"Failed to get nss.h history: {e}")
if not result:
return None
commits = [c
for c
in result.split(
"\n")
if c]
# hg log output is newest first
# Search from oldest to newest to find the first commit showing this version as beta
for commit
in reversed(commits):
try:
content = check_output(
[
"hg",
"cat",
"-r", commit,
"lib/nss/nss.h"]
).decode()
except Exception:
continue
if (
re.search(rf
"#define\s+NSS_VMAJOR\s+{re.escape(major)}\b", content)
and re.search(rf
"#define\s+NSS_VMINOR\s+{re.escape(minor)}\b", content)
and re.search(r
"#define\s+NSS_BETA\s+PR_TRUE", content)
):
return commit
return None
def extract_bug_ids(bug_lines):
"""Extract integer bug IDs from formatted 'Bug N - description.' lines."""
seen = set()
ids = []
for line
in bug_lines:
m = re.match(r
"Bug\s+(\d+)", line, re.IGNORECASE)
if m:
bid = int(m.group(
1))
if bid
not in seen:
seen.add(bid)
ids.append(bid)
return ids
def get_bug_list_for_version(version):
"""Extract bug changes from Mercurial log for the given release version.
For point releases (a.b.c), uses the previous RTM tag (NSS_A_B_RTM
for
3.x.
1, NSS_A_B_{C-
1}_RTM
for later point releases)
as the base.
For regular releases (a.b), finds the commit that bumped lib/nss/nss.h to
version a.b Beta
and collects all commits after it on the release branch.
Falls back to comparing
with NSS_A_(B-
1)_BRANCH
if no beta bump
is found.
"""
parts = version.split(
".")
if len(parts) <
2:
exit_with_failure(f
"Invalid version format: {version}")
major, minor = parts[
0], parts[
1]
branch_name = f
"NSS_{major}_{minor}_BRANCH"
rtm_tag = version_string_to_RTM_tag(version)
if tag_exists(rtm_tag):
upper = rtm_tag
elif tag_exists(branch_name):
upper = branch_name
elif default_branch_has_version(major, minor):
print(f
"Branch {branch_name} not yet created; using default branch.")
upper =
"default"
else:
exit_with_failure(
f
"Cannot find branch {branch_name} and default does not contain NSS {version}."
)
if len(parts) ==
3:
patch = int(parts[
2])
prev_version = (
f
"{major}.{minor}" if patch ==
1 else f
"{major}.{minor}.{patch - 1}"
)
prev_tag = version_string_to_RTM_tag(prev_version)
print(f
"Point release: collecting commits after {prev_tag} on {upper}.")
revset = f
"descendants('{prev_tag}') and ::'{upper}' and not '{prev_tag}'"
grafted_away = set()
else:
prev_branch = f
"NSS_{major}_{int(minor) - 1}_BRANCH"
revset = f
"::'{upper}' and not ::'{prev_branch}'"
beta_bump = find_beta_bump_commit(upper, prev_branch, major, minor)
if beta_bump:
print(f
"Found {version} beta version bump at {beta_bump}.")
# Commits before the beta bump may have been grafted to the previous
# branch for a point release; those belong to the previous version.
grafted_to_prev = graft_sources_on_branch(prev_branch)
pre_bump_nodes = node_set(
f
"::'{beta_bump}' and not ::'{prev_branch}' and not '{beta_bump}'"
)
grafted_away = pre_bump_nodes & grafted_to_prev
else:
print(f
"Warning: No {version} beta version bump found in lib/nss/nss.h.")
grafted_away = set()
try:
command = [
"hg",
"log",
"-r",
revset,
"--template",
"{node|short}\\t{desc|firstline}\\n",
]
log_output = check_output(command).decode(
"utf-8")
except Exception
as e:
exit_with_failure(f
"Failed to get hg log: {e}")
commits = []
for line
in log_output.split(
"\n"):
if "\t" in line:
node, title = line.split(
"\t",
1)
commits.append((node.strip(), title.strip()))
# Collect hashes that were backed out within this range
backed_out = set()
for _node, title
in commits:
m = re.search(r
"\bBacked out changeset ([0-9a-f]+)\b", title, re.IGNORECASE)
if m:
backed_out.add(m.group(
1))
bug_lines = []
for node, title
in commits:
if node
in backed_out:
continue # this commit was backed out; skip it
if re.search(r
"\bBacked out changeset\b", title, re.IGNORECASE):
continue # this is a backout commit; skip it
if node
in grafted_away:
continue # pre-bump commit grafted to previous branch; belongs to previous version
if not re.match(r
"bug\s+\d+", title, re.IGNORECASE):
continue
line = title
# Remove reviewer info from tail: r=, r?, or r! (with optional leading comma)
line = re.sub(r
",?\s+r[=?!]\S+$",
"", line).strip()
# Normalize "Bug N:" to "Bug N -"
line = re.sub(r
"(Bug\s+\d+):", r
"\1 -", line, flags=re.IGNORECASE)
# Ensure "Bug N - description" format (add dash if missing)
line = re.sub(r
"(Bug\s+\d+)\s+([^-])", r
"\1 - \2", line, flags=re.IGNORECASE)
line = line.rstrip(
",").strip()
if line
and not line.endswith(
"."):
line = line +
"."
if line
and line
not in bug_lines:
bug_lines.append(line)
return list(reversed(bug_lines))