dongjoon-hyun commented on code in PR #60031:
URL: https://github.com/apache/spark/pull/60031#discussion_r4448436183


##########
dev/merge_spark_pr.py:
##########
@@ -2283,6 +2285,24 @@ def __str__(self) -> str:
         return parts + (" " if parts else "") + self.text
 
 
+def _unwrap_wrappers(title: str) -> tuple[str, list[str]]:
+    """Return the innermost title and the wrapper words 
(``Revert``/``Reapply``), outermost first.
+
+    See :func:`_unwrap_title` for how the wrappers are stripped.
+
+    >>> _unwrap_wrappers('Revert "Reapply "[SPARK-1234][SQL] Fix""')
+    ('[SPARK-1234][SQL] Fix', ['Revert', 'Reapply'])
+    >>> _unwrap_wrappers('[SPARK-1234][SQL] Fix')
+    ('[SPARK-1234][SQL] Fix', [])
+    """
+    title = title.strip()
+    words = []
+    while m := _WRAPPER_RE.fullmatch(title):

Review Comment:
   **[1] Unbalanced nested quotes fall back to the outermost wrapper.**
   
   A nested title with one closing quote missing is still classified by the 
layers that do balance, so the parity check silently falls back to the old 
outermost-only decision.
   
   For example, a hand-edited re-land titled `Revert "Revert "[SPARK-N][SQL] 
X"` (one `"` dropped): the outer layer matches, but the inner `Revert 
"[SPARK-N][SQL] X` does not end with `"`, so `words == ['Revert']` and 
`update_jira_for_pr` takes the revert path. The leftover title also does not 
start with a bracket, so `leading_jira_ids` returns `[]` and the committer gets 
an empty JIRA-id prompt, and an inner `[MINOR]` would not be honored either.
   
   Since the direction now depends on the full nesting, could `main()` fail (or 
at least warn) when the innermost title still starts with a wrapper word? 
Something like:
   
   ```python
   inner, words = _unwrap_wrappers(title)
   if words and inner.startswith(('Revert "', 'Reapply "')):
       fail("Unbalanced Revert/Reapply quotes in PR title: %s" % title)
   ```



##########
dev/merge_spark_pr.py:
##########
@@ -1994,6 +1994,8 @@ def update_jira_for_pr(pr_num, title, merge_branches, 
title_components, is_rever
     # initialize_jira() fails fast on a missing library/credentials when JIRA 
is live.
     print()
     continue_maybe("Would you like to update an associated JIRA?")
+    # A revert of the innermost change, decided by all the wrappers (see 
classify_wrapped_title).
+    _, is_revert = classify_wrapped_title(title)

Review Comment:
   **[2] The direction is only known after the push.**
   
   This one is not introduced by this PR, so feel free to leave it for a 
separate JIRA.
   
   The direction is decided here, after the merge has been pushed. Before the 
push, `check_linked_jira_issues` still shows every wrapped title as a fresh 
merge:
   
   - For `Revert "[SPARK-N] X"` it prints `WARNING: already Resolved/Closed`, 
which is the expected state for a revert.
   - For `Revert "Revert "[SPARK-N] X""` nothing tells the committer that the 
script will resolve rather than revert.
   
   Deciding the direction once in `main()`, showing it in the pre-merge 
summary, and passing it here would let the committer catch a wrong direction 
before the irreversible step.



##########
dev/merge_spark_pr.py:
##########
@@ -2138,7 +2140,7 @@ def normalize(cls, token):
 # A SPARK id that opens a title without brackets, e.g. the old subject 
"SPARK-49004".
 _BARE_SPARK_ID_RE = re.compile(r"SPARK-[0-9]+\b", re.IGNORECASE | re.ASCII)
 # A Revert "..." / Reapply "..." wrapper, which main() keeps verbatim in the 
PR title.

Review Comment:
   **[4] "Verbatim" is no longer accurate.**
   
   This PR strips surrounding whitespace from wrapped titles and changes two 
comments in `main()` from "verbatim" to "as is", but a few other places still 
say that `main()` keeps them verbatim:
   
   - this comment
   - L1975 in `update_jira_for_pr`
   - L2309 in the `_unwrap_title` docstring
   - L1165 in the `title_similarity` docstring
   
   Now that `' Revert "[SPARK-N] X" '` is committed as `Revert "[SPARK-N] X"`, 
could these be updated too?



##########
dev/merge_spark_pr.py:
##########
@@ -2307,10 +2327,56 @@ def _unwrap_title(title: str) -> str:
     >>> _unwrap_title('Revert "')
     'Revert "'
     """
-    title = title.strip()
-    while m := _WRAPPER_RE.fullmatch(title):
-        title = m.group(1).strip()
-    return title
+    return _unwrap_wrappers(title)[0]
+
+
+def classify_wrapped_title(title: str) -> tuple[bool, bool]:
+    """Return ``(is_wrapped, is_revert)`` for a PR title.
+
+    ``is_wrapped`` is true when the title has a ``Revert "..."`` / ``Reapply 
"..."``
+    wrapper (see :func:`_unwrap_title`); ``main()`` keeps such titles as they 
are,
+    apart from surrounding whitespace.
+    ``is_revert`` is true when the wrappers net to a revert of the innermost 
change,
+    which ``update_jira_for_pr`` handles by removing fix versions instead of 
resolving.
+    The direction is decided by all the wrappers, not only the outermost one: 
each
+    ``Revert`` flips it, while a ``Reapply``, which git uses for a revert of a 
revert,
+    flips it twice and so leaves it unchanged. So the title is a revert when 
it has an
+    odd number of ``Revert`` wrappers.
+
+    >>> for t in ('[SPARK-1234][SQL] Fix',
+    ...           'Revert "[SPARK-1234][SQL] Fix"',
+    ...           'Reapply "[SPARK-1234][SQL] Fix"',
+    ...           'Revert "Revert "[SPARK-1234][SQL] Fix""',
+    ...           'Revert "Reapply "[SPARK-1234][SQL] Fix""',
+    ...           'Reapply "Revert "[SPARK-1234][SQL] Fix""',
+    ...           'Reapply "Reapply "[SPARK-1234][SQL] Fix""'):
+    ...     print(classify_wrapped_title(t), t)
+    (False, False) [SPARK-1234][SQL] Fix
+    (True, True) Revert "[SPARK-1234][SQL] Fix"
+    (True, False) Reapply "[SPARK-1234][SQL] Fix"
+    (True, False) Revert "Revert "[SPARK-1234][SQL] Fix""
+    (True, True) Revert "Reapply "[SPARK-1234][SQL] Fix""
+    (True, True) Reapply "Revert "[SPARK-1234][SQL] Fix""
+    (True, False) Reapply "Reapply "[SPARK-1234][SQL] Fix""
+
+    Three levels of nesting, which a doctest in a docstring cannot spell 
literally:
+
+    >>> q = '"'
+    >>> classify_wrapped_title('Revert "Revert "Revert "[SPARK-1234] Fix' + q 
* 3)
+    (True, True)
+    >>> classify_wrapped_title('Revert "Reapply "Revert "[SPARK-1234] Fix' + q 
* 3)
+    (True, False)
+
+    Surrounding whitespace is ignored, as in :func:`_unwrap_title`. A bare 
``Revert "``
+    is not a wrapper, so ``main()`` parses it as an ordinary title and rejects 
it.
+
+    >>> classify_wrapped_title(' Revert "[SPARK-1234][SQL] Fix" ')
+    (True, True)
+    >>> classify_wrapped_title('Revert "'), classify_wrapped_title('Reapply "')
+    ((False, False), (False, False))
+    """
+    _, words = _unwrap_wrappers(title)

Review Comment:
   **[3] Each caller discards half of the tuple.**
   
   `main()` keeps only `is_wrapped` (`is_wrapped_pr, _ = ...`) and 
`update_jira_for_pr` keeps only `is_revert` (`_, is_revert = ...`). A tuple 
where every caller drops one element, next to `_unwrap_title` and 
`_unwrap_wrappers`, is more API than needed. How about a single predicate?
   
   ```python
   def _is_revert_title(title: str) -> bool:
       return _unwrap_wrappers(title)[1].count("Revert") % 2 == 1
   ```
   
   `main()` could then use `bool(_unwrap_wrappers(title)[1])`, and the parity 
explanation in this docstring would move to the predicate.



##########
dev/merge_spark_pr.py:
##########
@@ -2307,10 +2327,56 @@ def _unwrap_title(title: str) -> str:
     >>> _unwrap_title('Revert "')
     'Revert "'
     """
-    title = title.strip()
-    while m := _WRAPPER_RE.fullmatch(title):
-        title = m.group(1).strip()
-    return title
+    return _unwrap_wrappers(title)[0]
+
+
+def classify_wrapped_title(title: str) -> tuple[bool, bool]:
+    """Return ``(is_wrapped, is_revert)`` for a PR title.
+
+    ``is_wrapped`` is true when the title has a ``Revert "..."`` / ``Reapply 
"..."``
+    wrapper (see :func:`_unwrap_title`); ``main()`` keeps such titles as they 
are,
+    apart from surrounding whitespace.
+    ``is_revert`` is true when the wrappers net to a revert of the innermost 
change,
+    which ``update_jira_for_pr`` handles by removing fix versions instead of 
resolving.
+    The direction is decided by all the wrappers, not only the outermost one: 
each
+    ``Revert`` flips it, while a ``Reapply``, which git uses for a revert of a 
revert,
+    flips it twice and so leaves it unchanged. So the title is a revert when 
it has an
+    odd number of ``Revert`` wrappers.
+
+    >>> for t in ('[SPARK-1234][SQL] Fix',
+    ...           'Revert "[SPARK-1234][SQL] Fix"',
+    ...           'Reapply "[SPARK-1234][SQL] Fix"',
+    ...           'Revert "Revert "[SPARK-1234][SQL] Fix""',
+    ...           'Revert "Reapply "[SPARK-1234][SQL] Fix""',
+    ...           'Reapply "Revert "[SPARK-1234][SQL] Fix""',
+    ...           'Reapply "Reapply "[SPARK-1234][SQL] Fix""'):
+    ...     print(classify_wrapped_title(t), t)
+    (False, False) [SPARK-1234][SQL] Fix
+    (True, True) Revert "[SPARK-1234][SQL] Fix"
+    (True, False) Reapply "[SPARK-1234][SQL] Fix"
+    (True, False) Revert "Revert "[SPARK-1234][SQL] Fix""
+    (True, True) Revert "Reapply "[SPARK-1234][SQL] Fix""
+    (True, True) Reapply "Revert "[SPARK-1234][SQL] Fix""
+    (True, False) Reapply "Reapply "[SPARK-1234][SQL] Fix""
+
+    Three levels of nesting, which a doctest in a docstring cannot spell 
literally:

Review Comment:
   **[6] Three-level titles can be spelled directly.**
   
   This docstring is not raw, so escaped quotes put `"""` into the doctest 
source, and the three-level cases can be written like the others:
   
   ```python
       >>> classify_wrapped_title('Revert "Revert "Revert "[SPARK-1234] 
Fix\"\"\"')
       (True, True)
   ```
   
   I checked that a doctest written this way runs. That would drop the `q` 
helper and this note.



##########
dev/merge_spark_pr.py:
##########
@@ -2652,23 +2718,25 @@ def main():
     pr_events = get_json("%s/issues/%s/events" % (GITHUB_API_BASE, pr_num))
 
     url = pr["url"]
-    title = pr["title"]
+    # Strip surrounding whitespace, which classify_wrapped_title() ignores, so 
that a wrapped
+    # title, kept as is below, is committed without it.
+    title = pr["title"].strip()

Review Comment:
   **[5] The `main()` changes have no test.**
   
   The strip here, the `is_wrapped_pr` switch, and moving the `Normalized 
title` print out of the `if not is_wrapped_pr` block are all in `main()`, which 
the doctests don't reach (as the PR description notes). A regression such as 
classifying `pr["title"]` instead of the stripped title, or re-indenting the 
print, would pass every doctest.
   
   Moving the title handling into a small helper (PR title in, `(title, 
title_components)` out) would let a doctest pin the whitespace-wrapped and bare 
`Revert "` cases that the description promises.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to