Author: dsahlberg
Date: Mon Mar 10 08:52:46 2025
New Revision: 1924264
URL: http://svn.apache.org/viewvc?rev=1924264&view=rev
Log:
New script to automate nominating backports, replacing nominate.pl.
* tools/dist/nominate-backport.py
New file, implementing the nomination.
* tools/dist/README.backport
Document the new script.
Added:
subversion/trunk/tools/dist/nominate-backport.py (with props)
Modified:
subversion/trunk/tools/dist/README.backport
Modified: subversion/trunk/tools/dist/README.backport
URL:
http://svn.apache.org/viewvc/subversion/trunk/tools/dist/README.backport?rev=1924264&r1=1924263&r2=1924264&view=diff
==============================================================================
--- subversion/trunk/tools/dist/README.backport (original)
+++ subversion/trunk/tools/dist/README.backport Mon Mar 10 08:52:46 2025
@@ -48,6 +48,9 @@ merge-approved-backports.py:
automatically merge approved backports, see documentation in PMC private
repo.
+nominate-backport.py:
+ Implementation of [F4] using backport.py.
+
backport_tests_py.py:
Regression tests for detect-conflicting-backports.py and
merge-approved-backports.py
Added: subversion/trunk/tools/dist/nominate-backport.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/tools/dist/nominate-backport.py?rev=1924264&view=auto
==============================================================================
--- subversion/trunk/tools/dist/nominate-backport.py (added)
+++ subversion/trunk/tools/dist/nominate-backport.py Mon Mar 10 08:52:46 2025
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""\
+Nominate revision(s) for backport.
+
+This script should be run interactively, to nominate code for backport.
+
+Run this script from the root of a stable branch's working copy (e.g.,
+a working copy of /branches/1.9.x). This script will add an entry to the
+STATUS file and optionally commit the changes.
+"""
+
+import sys
+assert sys.version_info[0] == 3, "This script targets Python 3"
+
+import os
+import subprocess
+import hashlib
+import string
+import re
+import textwrap
+
+import backport.merger
+import backport.status
+
+# Constants
+STATUS = './STATUS'
+LINELENGTH = 79
+
+def subprocess_output(args):
+ result = subprocess.run(args, capture_output = True, text = True)
+ return result.stdout
+
+def check_local_mods_to_STATUS():
+ status = subprocess_output(['svn', 'diff', './STATUS'])
+ if status != "":
+ print(f"Local mods to STATUS file {STATUS}")
+ print(status)
+ if YES:
+ sys.exit(1)
+ input("Press Enter to continue or Ctrl-C to abort...")
+ return True
+
+ return False
+
+def get_availid():
+ """Try to get the AVAILID of the current user"""
+
+ SVN_A_O_REALM = '<https://svn.apache.org:443> ASF Committers'
+
+ try:
+ # First try to get the ID from an environment variable
+ return os.environ["AVAILID"]
+
+ except KeyError:
+ try:
+ # Failing, try executing svn auth
+ auth = subprocess_output(['svn', 'auth', 'svn.apache.org:443'])
+ correct_realm = False
+ for line in auth.split('\n'):
+ line = line.strip()
+ if line.startswith('Authentication realm:'):
+ correct_realm = line.find(SVN_A_O_REALM)
+ elif line.startswith('Username:'):
+ return line[10:]
+
+ except OSError as e:
+ try:
+ # Last resort, read from ~/.subversion/auth/svn.simple
+ dir = os.environ["HOME"] + "/.subversion/auth/svn.simple/"
+ filename = hashlib.md5(SVN_A_O_REALM.encode('utf-8')).hexdigest()
+ with open(dir+filename, 'r') as file:
+ lines = file.readlines()
+ for i in range(0, len(lines), 4):
+ if lines[i].strip() == "K 8" and lines[i+1].strip() == 'username':
+ return lines[i+3]
+
+ except:
+ raise
+ except:
+ raise
+ except:
+ raise
+
+def usage():
+ print(f"""nominate-backport.py: a tool for adding entries to STATUS.
+
+Usage: ./tools/dist/nominate-backport.py "r42, r43, r45" "$Some_justification"
+
+Will add:
+ * r42, r43, r45
+ (log message of r42)
+ Justification:
+ $Some_justification
+ Votes:
+ +1: {AVAILID}
+to STATUS. Backport branches are detected automatically.
+
+The revisions argument may contain arbitrary text (besides the revision
+numbers); it will be ignored. For example,
+ ./tools/dist/nominate-backport.py "Committed revision 42." \\
+ "$Some_justification"
+will nominate r42.
+
+Revision numbers within the last thousand revisions may be specified using
+the last three digits only.
+
+The justification can be an arbitrarily-long string; if it is wider than the
+available width, this script will wrap it for you (and allow you to review
+the result before committing).
+
+The STATUS file in the current directory is used.
+ """)
+
+def warned_cannot_commit(message):
+ if AVAILID is None:
+ print(message + ": Unable to determine your username via $AVAILID or svn
auth or ~/.subversion/auth/.")
+ return True
+ return False
+
+def main():
+ # Pre-requisite
+ if warned_cannot_commit("Nominating failed"):
+ print("Unable to proceed.\n")
+ sys.exit(1)
+ had_local_mods = check_local_mods_to_STATUS()
+
+ # Update existing status file and load it
+ backport.merger.run_svn_quiet(['update'])
+ sf = backport.status.StatusFile(open(STATUS, encoding="UTF-8"))
+
+ # Argument parsing.
+ if len(sys.argv) < 3:
+ usage()
+ return
+ revisions = [int(''.join(filter(str.isdigit, revision))) for revision in
sys.argv[1].split()]
+ justification = sys.argv[2]
+
+ # Get some WC info
+ info = subprocess_output(['svn', 'info'])
+ BASE_revision = ""
+ URL = ""
+ for line in info.split('\n'):
+ if line.startswith('URL:'):
+ URL = line.split('URL:')[1]
+ elif line.startswith('Revision:'):
+ BASE_revision = line.split('Revision:')[1]
+
+ # To save typing, require just the last three digits if they're unambiguous.
+ if BASE_revision != "":
+ BASE_revision = int(BASE_revision)
+ if BASE_revision > 1000:
+ residue = BASE_revision % 1000
+ thousands = BASE_revision - residue
+ revisions = [r+thousands if r<1000 else r for r in revisions]
+
+ # Deduplicate and sort
+ revisions = list(set(revisions))
+ revisions.sort()
+
+ # Determine whether a backport branch exists
+ branch = subprocess_output(['svn', 'info', '--show-item', 'url', '--',
URL+'-r'+str(revisions[0])]).replace('\n', '')
+ if branch == "":
+ branch = None
+
+ # Get log message from first revision
+ logmsg = subprocess_output(['svn', 'propget', '--revprop', '-r',
+ str(revisions[0]), '--strict', 'svn:log', '^/'])
+ if (logmsg == ""):
+ print("Can't fetch log message of r" + revisions[0])
+ sys.exit(1)
+
+ # Delete all leading empty lines
+ split_logmsg = logmsg.split("\n")
+ for line in split_logmsg:
+ if line == "":
+ del split_logmsg[0]
+ else:
+ break
+
+ # If the first line is a file, ie: "* file"
+ # Then we expect the next line to be " (symbol): Log message."
+ # Remove "* file" and " (symbol):" so we can use this log message.
+ if split_logmsg[0].startswith("* "):
+ del split_logmsg[0]
+ split_logmsg[0] = re.sub(r".*\): ", "", split_logmsg[0])
+
+ # Get the log message summary, up to the first empty line or the
+ # next file nomination.
+ logmsg = ""
+ for i in range(len(split_logmsg)):
+ if split_logmsg[i].strip() == "" \
+ or split_logmsg[i].strip().startswith("* "):
+ break
+ logmsg += split_logmsg[i].strip() + " "
+
+ # Create new status entry and add to STATUS
+ e = backport.status.StatusEntry(None)
+ e.revisions = revisions
+ e.logsummary = textwrap.wrap(logmsg)
+ e.justification_str = "\n" + textwrap.fill(justification, initial_indent='
', subsequent_indent=' ') + "\n"
+ e.votes_str = f" +1: {AVAILID}\n"
+ e.branch = branch
+ sf.insert(e, "Candidate changes")
+
+ # Write new STATUS file
+ with open(STATUS, mode='w', encoding="UTF-8") as f:
+ sf.unparse(f)
+
+ # Check for changes to commit
+ diff = subprocess_output(['svn', 'diff', STATUS])
+ print(diff)
+ answer = input("Commit this nomination [y/N]? ")
+ if answer.lower() == "y":
+ subprocess_output(['svn', 'commit', STATUS, '-m',
+ '* STATUS: Nominate r' +
+ ', r'.join(map(str, revisions))])
+ else:
+ answer = input("Revert STATUS (destroying local mods) [y/N]? ")
+ if answer.lower() == "y":
+ subprocess_output(['svn', 'revert', STATUS])
+
+ sys.exit(0)
+
+AVAILID = get_availid()
+
+# Load the various knobs
+try:
+ YES = True if os.environ["YES"].lower() in ["true", "1", "yes"] else False
+except:
+ YES = False
+
+try:
+ MAY_COMMIT = True if os.environ["MAY_COMMIT"].lower() in ["true", "1",
"yes"] else False
+except:
+ MAY_COMMIT = False
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ print("\n")
+ sys.exit(1)
Propchange: subversion/trunk/tools/dist/nominate-backport.py
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: subversion/trunk/tools/dist/nominate-backport.py
------------------------------------------------------------------------------
svn:executable = *