Skip to content

Commit 5b780b6

Browse files
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. git-svn-id: https://svn.apache.org/repos/asf/subversion/trunk@1924264 13f79535-47bb-0310-9956-ffa450edef68
1 parent a251de5 commit 5b780b6

File tree

2 files changed

+263
-0
lines changed

2 files changed

+263
-0
lines changed

tools/dist/README.backport

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ merge-approved-backports.py:
4848
automatically merge approved backports, see documentation in PMC private
4949
repo.
5050

51+
nominate-backport.py:
52+
Implementation of [F4] using backport.py.
53+
5154
backport_tests_py.py:
5255
Regression tests for detect-conflicting-backports.py and merge-approved-backports.py
5356

tools/dist/nominate-backport.py

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#!/usr/bin/env python3
2+
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
20+
"""\
21+
Nominate revision(s) for backport.
22+
23+
This script should be run interactively, to nominate code for backport.
24+
25+
Run this script from the root of a stable branch's working copy (e.g.,
26+
a working copy of /branches/1.9.x). This script will add an entry to the
27+
STATUS file and optionally commit the changes.
28+
"""
29+
30+
import sys
31+
assert sys.version_info[0] == 3, "This script targets Python 3"
32+
33+
import os
34+
import subprocess
35+
import hashlib
36+
import string
37+
import re
38+
import textwrap
39+
40+
import backport.merger
41+
import backport.status
42+
43+
# Constants
44+
STATUS = './STATUS'
45+
LINELENGTH = 79
46+
47+
def subprocess_output(args):
48+
result = subprocess.run(args, capture_output = True, text = True)
49+
return result.stdout
50+
51+
def check_local_mods_to_STATUS():
52+
status = subprocess_output(['svn', 'diff', './STATUS'])
53+
if status != "":
54+
print(f"Local mods to STATUS file {STATUS}")
55+
print(status)
56+
if YES:
57+
sys.exit(1)
58+
input("Press Enter to continue or Ctrl-C to abort...")
59+
return True
60+
61+
return False
62+
63+
def get_availid():
64+
"""Try to get the AVAILID of the current user"""
65+
66+
SVN_A_O_REALM = '<https://svn.apache.org:443> ASF Committers'
67+
68+
try:
69+
# First try to get the ID from an environment variable
70+
return os.environ["AVAILID"]
71+
72+
except KeyError:
73+
try:
74+
# Failing, try executing svn auth
75+
auth = subprocess_output(['svn', 'auth', 'svn.apache.org:443'])
76+
correct_realm = False
77+
for line in auth.split('\n'):
78+
line = line.strip()
79+
if line.startswith('Authentication realm:'):
80+
correct_realm = line.find(SVN_A_O_REALM)
81+
elif line.startswith('Username:'):
82+
return line[10:]
83+
84+
except OSError as e:
85+
try:
86+
# Last resort, read from ~/.subversion/auth/svn.simple
87+
dir = os.environ["HOME"] + "/.subversion/auth/svn.simple/"
88+
filename = hashlib.md5(SVN_A_O_REALM.encode('utf-8')).hexdigest()
89+
with open(dir+filename, 'r') as file:
90+
lines = file.readlines()
91+
for i in range(0, len(lines), 4):
92+
if lines[i].strip() == "K 8" and lines[i+1].strip() == 'username':
93+
return lines[i+3]
94+
95+
except:
96+
raise
97+
except:
98+
raise
99+
except:
100+
raise
101+
102+
def usage():
103+
print(f"""nominate-backport.py: a tool for adding entries to STATUS.
104+
105+
Usage: ./tools/dist/nominate-backport.py "r42, r43, r45" "$Some_justification"
106+
107+
Will add:
108+
* r42, r43, r45
109+
(log message of r42)
110+
Justification:
111+
$Some_justification
112+
Votes:
113+
+1: {AVAILID}
114+
to STATUS. Backport branches are detected automatically.
115+
116+
The revisions argument may contain arbitrary text (besides the revision
117+
numbers); it will be ignored. For example,
118+
./tools/dist/nominate-backport.py "Committed revision 42." \\
119+
"$Some_justification"
120+
will nominate r42.
121+
122+
Revision numbers within the last thousand revisions may be specified using
123+
the last three digits only.
124+
125+
The justification can be an arbitrarily-long string; if it is wider than the
126+
available width, this script will wrap it for you (and allow you to review
127+
the result before committing).
128+
129+
The STATUS file in the current directory is used.
130+
""")
131+
132+
def warned_cannot_commit(message):
133+
if AVAILID is None:
134+
print(message + ": Unable to determine your username via $AVAILID or svn auth or ~/.subversion/auth/.")
135+
return True
136+
return False
137+
138+
def main():
139+
# Pre-requisite
140+
if warned_cannot_commit("Nominating failed"):
141+
print("Unable to proceed.\n")
142+
sys.exit(1)
143+
had_local_mods = check_local_mods_to_STATUS()
144+
145+
# Update existing status file and load it
146+
backport.merger.run_svn_quiet(['update'])
147+
sf = backport.status.StatusFile(open(STATUS, encoding="UTF-8"))
148+
149+
# Argument parsing.
150+
if len(sys.argv) < 3:
151+
usage()
152+
return
153+
revisions = [int(''.join(filter(str.isdigit, revision))) for revision in sys.argv[1].split()]
154+
justification = sys.argv[2]
155+
156+
# Get some WC info
157+
info = subprocess_output(['svn', 'info'])
158+
BASE_revision = ""
159+
URL = ""
160+
for line in info.split('\n'):
161+
if line.startswith('URL:'):
162+
URL = line.split('URL:')[1]
163+
elif line.startswith('Revision:'):
164+
BASE_revision = line.split('Revision:')[1]
165+
166+
# To save typing, require just the last three digits if they're unambiguous.
167+
if BASE_revision != "":
168+
BASE_revision = int(BASE_revision)
169+
if BASE_revision > 1000:
170+
residue = BASE_revision % 1000
171+
thousands = BASE_revision - residue
172+
revisions = [r+thousands if r<1000 else r for r in revisions]
173+
174+
# Deduplicate and sort
175+
revisions = list(set(revisions))
176+
revisions.sort()
177+
178+
# Determine whether a backport branch exists
179+
branch = subprocess_output(['svn', 'info', '--show-item', 'url', '--', URL+'-r'+str(revisions[0])]).replace('\n', '')
180+
if branch == "":
181+
branch = None
182+
183+
# Get log message from first revision
184+
logmsg = subprocess_output(['svn', 'propget', '--revprop', '-r',
185+
str(revisions[0]), '--strict', 'svn:log', '^/'])
186+
if (logmsg == ""):
187+
print("Can't fetch log message of r" + revisions[0])
188+
sys.exit(1)
189+
190+
# Delete all leading empty lines
191+
split_logmsg = logmsg.split("\n")
192+
for line in split_logmsg:
193+
if line == "":
194+
del split_logmsg[0]
195+
else:
196+
break
197+
198+
# If the first line is a file, ie: "* file"
199+
# Then we expect the next line to be " (symbol): Log message."
200+
# Remove "* file" and " (symbol):" so we can use this log message.
201+
if split_logmsg[0].startswith("* "):
202+
del split_logmsg[0]
203+
split_logmsg[0] = re.sub(r".*\): ", "", split_logmsg[0])
204+
205+
# Get the log message summary, up to the first empty line or the
206+
# next file nomination.
207+
logmsg = ""
208+
for i in range(len(split_logmsg)):
209+
if split_logmsg[i].strip() == "" \
210+
or split_logmsg[i].strip().startswith("* "):
211+
break
212+
logmsg += split_logmsg[i].strip() + " "
213+
214+
# Create new status entry and add to STATUS
215+
e = backport.status.StatusEntry(None)
216+
e.revisions = revisions
217+
e.logsummary = textwrap.wrap(logmsg)
218+
e.justification_str = "\n" + textwrap.fill(justification, initial_indent=' ', subsequent_indent=' ') + "\n"
219+
e.votes_str = f" +1: {AVAILID}\n"
220+
e.branch = branch
221+
sf.insert(e, "Candidate changes")
222+
223+
# Write new STATUS file
224+
with open(STATUS, mode='w', encoding="UTF-8") as f:
225+
sf.unparse(f)
226+
227+
# Check for changes to commit
228+
diff = subprocess_output(['svn', 'diff', STATUS])
229+
print(diff)
230+
answer = input("Commit this nomination [y/N]? ")
231+
if answer.lower() == "y":
232+
subprocess_output(['svn', 'commit', STATUS, '-m',
233+
'* STATUS: Nominate r' +
234+
', r'.join(map(str, revisions))])
235+
else:
236+
answer = input("Revert STATUS (destroying local mods) [y/N]? ")
237+
if answer.lower() == "y":
238+
subprocess_output(['svn', 'revert', STATUS])
239+
240+
sys.exit(0)
241+
242+
AVAILID = get_availid()
243+
244+
# Load the various knobs
245+
try:
246+
YES = True if os.environ["YES"].lower() in ["true", "1", "yes"] else False
247+
except:
248+
YES = False
249+
250+
try:
251+
MAY_COMMIT = True if os.environ["MAY_COMMIT"].lower() in ["true", "1", "yes"] else False
252+
except:
253+
MAY_COMMIT = False
254+
255+
if __name__ == "__main__":
256+
try:
257+
main()
258+
except KeyboardInterrupt:
259+
print("\n")
260+
sys.exit(1)

0 commit comments

Comments
 (0)