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])


Reply via email to