Skip to content

Commit e3c3b86

Browse files
committed
Add a new script implementing the "F3. Reviewing STATUS nominations and casting
votes." function to the backport.py family. It isn't fully tested yet and has not reached feature parity with backport.pl but it seems to work for the most basic stuff. * tools/dist/manage-backports.py (): New file git-svn-id: https://svn.apache.org/repos/asf/subversion/trunk@1925158 13f79535-47bb-0310-9956-ffa450edef68
1 parent 9263eea commit e3c3b86

1 file changed

Lines changed: 356 additions & 0 deletions

File tree

tools/dist/manage-backports.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
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+
import backport.wc
43+
44+
# Constants
45+
STATUS = './STATUS'
46+
LINELENGTH = 79
47+
48+
if os.name == 'nt':
49+
try:
50+
SHELL = os.environ['COMSPEC']
51+
except KeyError:
52+
print("Can't find %COMSPEC%\n")
53+
sys.exit(1)
54+
elif os.name == 'posix':
55+
try:
56+
SHELL = os.environ['SHELL']
57+
except KeyError:
58+
print("Can't find $SHELL\n")
59+
sys.exit(1)
60+
else:
61+
print("Unknown os.name (" + os.name + "), can't find SHELL")
62+
sys.exit(1)
63+
64+
def subprocess_output(args):
65+
result = subprocess.run(args, capture_output = True, text = True)
66+
return result.stdout
67+
68+
def check_local_mods_to_STATUS():
69+
status = subprocess_output(['svn', 'diff', './STATUS'])
70+
if status != "":
71+
print(f"Local mods to STATUS file {STATUS}")
72+
print(status)
73+
if YES:
74+
sys.exit(1)
75+
input("Press Enter to continue or Ctrl-C to abort...")
76+
return True
77+
78+
return False
79+
80+
def get_availid():
81+
"""Try to get the AVAILID of the current user"""
82+
83+
SVN_A_O_REALM = '<https://svn.apache.org:443> ASF Committers'
84+
85+
try:
86+
# First try to get the ID from an environment variable
87+
return os.environ["AVAILID"]
88+
89+
except KeyError:
90+
try:
91+
# Failing, try executing svn auth
92+
auth = subprocess_output(['svn', 'auth', 'svn.apache.org:443'])
93+
correct_realm = False
94+
for line in auth.split('\n'):
95+
line = line.strip()
96+
if line.startswith('Authentication realm:'):
97+
correct_realm = line.find(SVN_A_O_REALM)
98+
elif line.startswith('Username:'):
99+
return line[10:]
100+
101+
except OSError as e:
102+
try:
103+
# Last resort, read from ~/.subversion/auth/svn.simple
104+
dir = os.environ["HOME"] + "/.subversion/auth/svn.simple/"
105+
filename = hashlib.md5(SVN_A_O_REALM.encode('utf-8')).hexdigest()
106+
with open(dir+filename, 'r') as file:
107+
lines = file.readlines()
108+
for i in range(0, len(lines), 4):
109+
if lines[i].strip() == "K 8" and lines[i+1].strip() == 'username':
110+
return lines[i+3]
111+
112+
except:
113+
raise
114+
except:
115+
raise
116+
except:
117+
raise
118+
119+
BACKPORT_OPTIONS_HELP=f"""y: Run a merge. It will not be committed.
120+
WARNING: This will run 'update' and 'revert -R ./'.
121+
l: Show logs for the entries being nominated.
122+
v: Show the full entry (the prompt only shows an abridged version).
123+
q: Quit the "for each entry" loop. If you have entered any votes or
124+
approvals, you will be prompted to commit them.
125+
±1: Enter a +1 or -1 vote
126+
You will be prompted to commit your vote at the end.
127+
±0: Enter a +0 or -0 vote
128+
You will be prompted to commit your vote at the end.
129+
a: Move the entry to the "Approved changes" section.
130+
When both approving and voting on an entry, approve first: for example,
131+
to enter a third +1 vote, type "a" "+" "1".
132+
e: Edit the entry in $EDITOR, which is '$EDITOR'.
133+
You will be prompted to commit your edits at the end.
134+
N: Move to the next entry. Do not prompt for the current entry again, even
135+
in future runs, unless the STATUS nomination has been modified (e.g.,
136+
revisions added, justification changed) in the repository.
137+
(This is a local action that will not affect other people or bots.)
138+
: Move to the next entry. Prompt for the current entry again in the next
139+
run of backport.pl.
140+
(That's a space character, ASCII 0x20.)
141+
?: Display this list.
142+
"""
143+
144+
BACKPORT_OPTIONS_MERGE_OPTIONS_HELP=f"""y: Open a shell.
145+
d: View a diff.
146+
N: Move to the next entry.
147+
?: Display this list.
148+
"""
149+
150+
def usage():
151+
print(f"""manage-backports.py: a tool for reviewing, merging, and voting on STATUS entries.
152+
153+
Normally, invoke this with CWD being the root of the stable branch (e.g.,
154+
1.8.x):
155+
156+
Usage: test -e $d/STATUS && cd $d && \\
157+
manage-backports.py [PATTERN]
158+
(where $d is a working copy of branches/1.8.x)
159+
160+
The ./STATUS file should be at HEAD with no local mods. Any local mods
161+
will be preserved through 'revert' operations but included in 'commit'
162+
operations.
163+
164+
If PATTERN is provided, only entries which match PATTERN are considered. The
165+
sense of "match" is either substring (fgrep) or Perl regexp (with /msi).
166+
167+
In interactive mode (the default), you will be prompted once per STATUS entry.
168+
At a prompt, you have the following options:
169+
170+
{BACKPORT_OPTIONS_HELP}
171+
172+
After running a merge, you have the following options:
173+
174+
{BACKPORT_OPTIONS_MERGE_OPTIONS_HELP}
175+
176+
To commit a merge, you have two options: either answer 'y' to the second prompt
177+
to open a shell, and manually run 'svn commit' therein; or set $MAY_COMMIT=1
178+
in the environment before running the script, in which case answering 'y'
179+
to the first prompt will not only run the merge but also commit it.
180+
181+
The 'svn' binary defined by the environment variable $SVN, or otherwise the
182+
'svn' found in $PATH, will be used to manage the working copy.
183+
""")
184+
185+
def warned_cannot_commit(message):
186+
if AVAILID is None:
187+
print(message + ": Unable to determine your username via $AVAILID or svn auth or ~/.subversion/auth/.")
188+
return True
189+
return False
190+
191+
def less(message):
192+
process = subprocess.Popen(["less"], stdin=subprocess.PIPE)
193+
try:
194+
process.stdin.write(message.encode('UTF-8'))
195+
process.communicate()
196+
except IOError as e:
197+
pass
198+
199+
def main():
200+
# Pre-requisite
201+
if warned_cannot_commit("Nominating failed"):
202+
print("Unable to proceed.\n")
203+
sys.exit(1)
204+
had_local_mods = check_local_mods_to_STATUS()
205+
206+
# Argument parsing.
207+
if len(sys.argv) > 1 and (sys.argv[1] == "-h" or sys.argv[1] == "--help"):
208+
usage()
209+
return
210+
211+
# Update existing status file and load it
212+
backport.merger.run_svn_quiet(['update'])
213+
try:
214+
sf = backport.status.StatusFile(open(STATUS, encoding="UTF-8"))
215+
except FileNotFoundError:
216+
print("STATUS file not found\n")
217+
sys.exit(1)
218+
219+
# Get wc info
220+
wcinfo = backport.wc.get_wc_info()
221+
222+
# Iterate the existing nominations
223+
for e in sf.entries_paras():
224+
# Display entry and check for user actions
225+
a = ""
226+
while a != "N":
227+
if a != "v":
228+
print("r" + ", r".join([str(r) for r in e._entry.revisions]))
229+
print(e._entry.justification_str)
230+
print(e._entry.votes_str)
231+
a = input("Run a merge? [y,l,v,q,±1,±0,a,e,N, ,?] ").strip()
232+
if a == "y":
233+
# Run a merge
234+
backport.merger.merge(e._entry, commit=False)
235+
while a != "N":
236+
a = input("Shall I open a subshell? [ydN?] ").strip()
237+
if a == "y":
238+
# Open Subshell
239+
subprocess.run([SHELL])
240+
elif a == "d":
241+
# Show diff
242+
(exit_code, stdout, stderr) = backport.merger.run_svn(["diff"])
243+
less(stdout)
244+
elif a == "N":
245+
# Next item
246+
break
247+
elif a == "?":
248+
# Help
249+
print(BACKPORT_OPTIONS_MERGE_OPTIONS_HELP)
250+
else:
251+
print("Please use one of the options in brackets (N to continue with next item)!")
252+
backport.merger.run_svn_quiet(["revert", ".", "--depth=infinity"])
253+
254+
elif a == "l":
255+
# Show logs for entries being nominated
256+
if e._entry.branch != None:
257+
backport.merger.run_svn(["log", "--stop-on-copy", "-v", "-g", "-r0:HEAD", "--", e._entry.branch])
258+
else:
259+
(error_code, stdout, stderr) = backport.merger.run_svn(["log", "--stop-on-copy", "-v", "-g", "-c" + ",".join([str(r) for r in e._entry.revisions]), "--", wcinfo["Repository_root"]])
260+
less(stdout)
261+
262+
elif a == "v":
263+
# Show the full entry
264+
print(e.entry())
265+
266+
elif a == "q":
267+
# Quit the "for each entry" loop.
268+
break
269+
270+
elif len(a) == 2 and a[0] in "+-" and a[1] in "01":
271+
print("Voting " + a)
272+
273+
elif a == "a":
274+
# Approve the entry
275+
sf.remove(e._entry)
276+
sf.insert(e._entry, "Approved changes")
277+
278+
elif a == "e":
279+
# Edit the entry in EDITOR
280+
subprocess.run([EDITOR, STATUS])
281+
282+
elif a == "N":
283+
# Move to next entry and don't prompt for this entry ever again
284+
break
285+
286+
elif a == "":
287+
# Move to next entry
288+
break
289+
290+
elif a == "?":
291+
# Print help
292+
print(BACKPORT_OPTIONS_HELP)
293+
294+
else:
295+
print("Please use one of the options in brackets (q to quit)!")
296+
297+
if a == "q":
298+
# Quit the "for each entry" loop.
299+
break
300+
301+
with open(STATUS, mode='w', encoding='UTF-8') as f:
302+
sf.unparse(f)
303+
sys.exit(0)
304+
305+
revisions = [int(''.join(filter(str.isdigit, revision))) for revision in sys.argv[1].split()]
306+
justification = sys.argv[2]
307+
308+
# Create new status entry and add to STATUS
309+
e = backport.status.StatusEntry(None)
310+
e.revisions = revisions
311+
e.logsummary = textwrap.wrap(logmsg)
312+
e.justification_str = "\n" + textwrap.fill(justification, initial_indent=' ', subsequent_indent=' ') + "\n"
313+
e.votes_str = f" +1: {AVAILID}\n"
314+
e.branch = branch
315+
sf.insert(e, "Candidate changes")
316+
317+
# Write new STATUS file
318+
with open(STATUS, mode='w', encoding="UTF-8") as f:
319+
sf.unparse(f)
320+
321+
# Check for changes to commit
322+
diff = subprocess_output(['svn', 'diff', STATUS])
323+
print(diff)
324+
answer = input("Commit this nomination [y/N]? ")
325+
if answer.lower() == "y":
326+
subprocess_output(['svn', 'commit', STATUS, '-m',
327+
'* STATUS: Nominate r' +
328+
', r'.join(map(str, revisions))])
329+
else:
330+
answer = input("Revert STATUS (destroying local mods) [y/N]? ")
331+
if answer.lower() == "y":
332+
subprocess_output(['svn', 'revert', STATUS])
333+
334+
sys.exit(0)
335+
336+
AVAILID = get_availid()
337+
338+
# Load the various knobs
339+
try:
340+
YES = True if os.environ["YES"].lower() in ["true", "1", "yes"] else False
341+
except:
342+
YES = False
343+
344+
try:
345+
MAY_COMMIT = True if os.environ["MAY_COMMIT"].lower() in ["true", "1", "yes"] else False
346+
except:
347+
MAY_COMMIT = False
348+
349+
if __name__ == "__main__":
350+
# print("Starting subshell!\n")
351+
352+
try:
353+
main()
354+
except KeyboardInterrupt:
355+
print("\n")
356+
sys.exit(1)

0 commit comments

Comments
 (0)