I'm by no means a Python expert and this was more of an excuse to learn
some more than an effort to make elegant code. Still, it works and the
recent nomination (r1924262) was done completely by this script.

I mean to continue to also implement the "review and vote" function of the
old backport.pl script in the near future and then remove the perl scripts.

Cheers,
Daniel




Den mån 10 mars 2025 kl 09:53 skrev <dsahlb...@apache.org>:

> 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 = *
>
>
>

Reply via email to