Author: dsahlberg
Date: Sat Mar 1 21:15:27 2025
New Revision: 1924120
URL: http://svn.apache.org/viewvc?rev=1924120&view=rev
Log:
Parse additional subheaders and recreate the StatusEntry from its attributes
instead of just outputting the original entry.
This adds some normalization of the recreated entry:
* List of revisions is a single line. Parser can accept line breaks.
* The branch name is listed on a separate line after the Branch subheader
and without ^/subversion/branches/ path. Parser can accept branch name
on the same line and with full path.
* No spaces on the line after a status entry. Parser can accept any number of
spaces.
* tools/dist/backport/status.py
(StatusEntry): New attributes justification_str, depends_str and notes_str
(StatusEntry.__init__): Parse the additional attributes and keep track of
some additional state required to recreate a faithful StatusEntry.
(StatusEntry.unparse): Remove obsolete comment (should have been moved to
__str__ in r1924119).
(StatusEntry.__str__): Create the StatusEntry from the individual attributes.
(Test_StatusEntry.test___init__): Modify two unittest where the normalization
caused failures. Also check the last line doesn't contain spaces.
Modified:
subversion/trunk/tools/dist/backport/status.py
Modified: subversion/trunk/tools/dist/backport/status.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/tools/dist/backport/status.py?rev=1924120&r1=1924119&r2=1924120&view=diff
==============================================================================
--- subversion/trunk/tools/dist/backport/status.py (original)
+++ subversion/trunk/tools/dist/backport/status.py Sat Mar 1 21:15:27 2025
@@ -336,8 +336,11 @@ class StatusEntry:
branch - the backport branch's basename, or None.
revisions - the revisions to nominated, as iterable of int.
logsummary - the text before the justification, as an array of lines.
+ justification_str - the justification, as an array of lines. An unparsed
string.
depends - true if a "Depends:" entry was found, False otherwise.
+ depends_str - everything after the "Depends:" subheader. An unparsed string.
accept - the value to pass to 'svn merge --accept=%s', or None.
+ notes_str - everything after the "Notes:" subheader. An unparsed string.
votes_str - everything after the "Votes:" subheader. An unparsed string.
"""
@@ -348,10 +351,14 @@ class StatusEntry:
STATUS_FILE is the StatusFile object containing this entry, if any.
"""
self.branch = None
+ self.branch_on_first_line = False
self.revisions = []
self.logsummary = []
+ self.justification_str = None
self.depends = False
+ self.depends_str = None
self.accept = None
+ self.notes_str = None
self.votes_str = None
self.status_file = status_file
@@ -366,8 +373,8 @@ class StatusEntry:
match = _re_entry_indentation.match(lines[0])
if not match:
raise ParseException("Entry found with no ' * ' line")
- indentation = len(match.group(1))
- lines = (line[indentation:] for line in lines)
+ self.indentation = len(match.group(1))
+ lines = (line[self.indentation:] for line in lines)
lines = (line.rstrip() for line in lines)
# Consume the generator.
@@ -378,6 +385,7 @@ class StatusEntry:
if match:
# Parse whichever group matched.
self.branch = self.parse_branch(match.group(1) or match.group(2))
+ self.branch_on_first_line = True
else:
while _re_revisions_line.match(lines[0]):
self.revisions.extend(map(int, re.compile(r'(\d+)').findall(lines[0])))
@@ -403,14 +411,26 @@ class StatusEntry:
else:
self.votes_str = None
- # depends, branch, notes
+ # depends, justification, branch, notes
while lines:
if lines[0].strip().startswith('Depends:'):
self.depends = True
+ self.depends_str = lines[0].strip()[8:].strip()
lines = lines[1:]
continue
+ if lines[0].strip().startswith('Justification:'):
+ self.justification_str = lines[0].strip().split(':', 1)[1] + "\n"
+ lines = lines[1:]
+
+ # Consume the indented body of the "Justification" field.
+ while lines and not lines[0][0].isalnum():
+ self.justification_str += lines[0] + "\n"
+ lines = lines[1:]
+
+ continue
+
if lines[0].strip().startswith('Branch:'):
maybe_value = lines[0].strip().split(':', 1)[1]
if maybe_value.strip():
@@ -427,16 +447,16 @@ class StatusEntry:
continue
if lines[0].strip().startswith('Notes:'):
- notes = lines[0].strip().split(':', 1)[1] + "\n"
+ self.notes_str = lines[0].strip().split(':', 1)[1] + "\n"
lines = lines[1:]
# Consume the indented body of the "Notes" field.
while lines and not lines[0][0].isalnum():
- notes += lines[0] + "\n"
+ self.notes_str += lines[0] + "\n"
lines = lines[1:]
# Look for possible --accept directives.
- matches = re.compile(r'--accept[ =]([a-z-]+)').findall(notes)
+ matches = re.compile(r'--accept[ =]([a-z-]+)').findall(self.notes_str)
if len(matches) > 1:
raise ParseException("Too many --accept values at %s" % (self,))
elif len(matches) == 1:
@@ -527,19 +547,42 @@ class StatusEntry:
def unparse(self, stream):
"Write this entry to STREAM, an open file-like object."
- # For now, this is simple.. until we add interactive editing.
stream.write(self.__str__())
def __str__(self):
- s = self.raw
+ indent = ''.ljust(self.indentation-2)
+ s = indent + '* '
+ if (len(self.revisions) > 0):
+ s += 'r' + ', r'.join(map(str, self.revisions)) + \
+ ('\n' + indent + ' ').join([""] + self.logsummary) + '\n'
+ else:
+ s += ('\n' + indent + ' ').join(self.logsummary) + '\n'
+ if self.justification_str is not None:
+ s += indent + ' Justification:' + \
+ ('\n' + indent + ' ').join(self.justification_str.split("\n")[:-1]) +
\
+ '\n'
+ if self.branch is not None and not self.branch_on_first_line:
+ s += indent + ' Branch:' + \
+ '\n' + indent + ' ' + self.branch + \
+ '\n'
+ if self.depends:
+ s += indent + ' Depends: ' + self.depends_str + '\n'
+ if self.notes_str is not None:
+ s += indent + ' Notes:' + \
+ ('\n' + indent + ' ').join(self.notes_str.split("\n")[:-1]) + \
+ '\n'
+ if self.votes_str is not None:
+ s += indent + ' Votes:' + \
+ ('\n' + indent + ' ').join([""] + self.votes_str.split("\n")[:-1]) +
"\n"
return s
class Test_StatusEntry(unittest.TestCase):
def test___init__(self):
"Test the entry parser"
- # All these entries actually have a "four spaces" line as their last line,
- # but the parser doesn't care.
+ # An entry may have spaces on its last line, the parser doesn't care.
+ # However unparse() will not replicate this, causing a failure on
+ # assertEqual(entry.unparse())
s = """\
* r42, r43,
@@ -550,8 +593,17 @@ class Test_StatusEntry(unittest.TestCase
Votes:
+1: jrandom
"""
+ # Revision list will be normalized to one line
+ sExpected = """\
+ * r42, r43, r44
+ This is the logsummary.
+ Branch:
+ 1.8.x-rfourty-two
+ Votes:
+ +1: jrandom
+"""
entry = StatusEntry(s)
- self.assertEqual(entry.__str__(), s)
+ self.assertEqual(entry.__str__(), sExpected)
self.assertEqual(entry.branch, "1.8.x-rfourty-two")
self.assertEqual(entry.revisions, [42, 43, 44])
self.assertEqual(entry.logsummary, ["This is the logsummary."])
@@ -574,7 +626,7 @@ class Test_StatusEntry(unittest.TestCase
Votes:
+1: jrandom
-1: jconstant
- """
+"""
entry = StatusEntry(s)
self.assertEqual(entry.__str__(), s)
self.assertIsNone(entry.branch)
@@ -597,7 +649,7 @@ class Test_StatusEntry(unittest.TestCase
Votes:
+1: jrandom
-1 (see <message-id>): jconstant
- """
+"""
entry = StatusEntry(s)
self.assertEqual(entry.__str__(), s)
self.assertEqual(entry.branch, "1.8.x-fixes")
@@ -611,8 +663,17 @@ class Test_StatusEntry(unittest.TestCase
Votes:
+1: jrandom
"""
+ # Normalizes branch (without path) to separate line
+ sExpected = """\
+ * r42
+ This is the logsummary.
+ Branch:
+ on-the-same-line
+ Votes:
+ +1: jrandom
+"""
entry = StatusEntry(s)
- self.assertEqual(entry.__str__(), s)
+ self.assertEqual(entry.__str__(), sExpected)
self.assertEqual(entry.branch, "on-the-same-line")
self.assertEqual(entry.revisions, [42])
@@ -624,7 +685,7 @@ class Test_StatusEntry(unittest.TestCase
This is the logsummary.
Votes:
+1: jrandom
- """
+"""
entry = StatusEntry(s)
self.assertEqual(entry.__str__(), s)
self.assertEqual(entry.branch, "1.8.x-fixes")
@@ -667,7 +728,7 @@ class Test_StatusEntry(unittest.TestCase
Fixes output that scripts depend on.
Votes:
+1: jrandom
- """
+"""
entry = StatusEntry(s)
self.assertEqual(entry.__str__(), s)
self.assertEqual(entry.revisions, [42])