sarutak commented on code in PR #60031:
URL: https://github.com/apache/spark/pull/60031#discussion_r4471183818


##########
dev/merge_spark_pr.py:
##########
@@ -2446,6 +2636,79 @@ def prompt_for_components():
         return components
 
 
+def prepare_merge_title(raw_title: str) -> tuple[str, list[str], bool]:
+    """Return ``(title, title_components, is_revert)`` for the title of the PR 
to merge.
+
+    Surrounding whitespace is stripped. A ``Revert "..."`` / ``Reapply "..."`` 
title is
+    otherwise kept as is: the wrapped title is an existing commit subject, 
which may
+    predate the current title rules, so it is not parsed and has no 
components. It is
+    rejected when the innermost title still starts with ``Revert`` or 
``Reapply``, e.g.
+    with a quote dropped: that wrapper would not be counted when deciding the 
direction
+    (see :func:`_is_net_revert`), and its JIRA id and skip tags would not be 
found
+    either. Any other title is parsed with :meth:`Title.parse`, which
+    fails on a malformed title, and its component tags are normalized, with a 
prompt for
+    a primary component when there is none. A title that changes is printed.
+    """
+    title = raw_title.strip()
+    inner, words = _unwrap_wrappers(title)

Review Comment:
   Done. Before merging, `check_wrapped_title()` fails with `Empty title inside 
the Revert/Reapply wrappers of the PR title: ...` when the innermost title is 
empty after stripping. The doctest covers `Revert ""`, `Revert " "`, `Reapply 
""`, and `Revert "Revert """`.
   



##########
dev/merge_spark_pr.py:
##########
@@ -3060,24 +3275,25 @@ def main():
     ...                    resolve_jira_issue=resolver, 
continue_maybe=confirm),
     ...         redirect_stdout(StringIO()),
     ...     ):
-    ...         update_jira_for_pr("123", title, branches, list(components), 
is_revert=is_revert)
+    ...         update_jira_for_pr("123", title, branches, list(components),
+    ...                            
is_revert=_is_net_revert(_unwrap_wrappers(title)[1]))

Review Comment:
   This is moot now, with [17]: `update_jira_for_pr` derives the direction from 
the title, and the helper passes only the title. What remains in `main()` is 
which title it passes. On a backport that now comes from 
`backport_jira_title()`, which has its own doctest (see [13]). No doctest 
reaches `main()`, so none of the lines this PR changes there is pinned by one: 
the `prepare_merge_title()` assignment, `check_wrapped_title()`, 
`backport_jira_title()`, and both `update_jira_for_pr` calls. I checked them 
with a mocked run of `main()` through the merge and the backport path, which 
runs the real `update_jira_for_pr`. It kills the three mutants there that 
survive the doctests: passing the raw PR title on a merge, which would drop a 
component entered at the prompt; passing the PR title on a backport; and not 
calling `check_wrapped_title()`.
   



##########
dev/merge_spark_pr.py:
##########
@@ -1960,41 +2008,124 @@ def resolve_jira_issues(jira_ids, merge_branches, 
comment, title_components=()):
         )
 
 
-def update_jira_for_pr(pr_num, title, merge_branches, title_components, 
is_revert=False):
-    if jira_disabled:
-        # JIRA was disabled at startup (unreachable); nothing to update.
-        print()
-        print_error(
-            f"JIRA is disabled; skipping JIRA update for PR #{pr_num}. "
-            f"Reconcile the ticket(s) manually."
-        )
-        return
+def jira_skip_tags(title):
+    """The title tags that make :func:`update_jira_for_pr` skip the JIRA 
update.
 
+    >>> jira_skip_tags('Revert "[MINOR][SQL] Fix"')
+    ['MINOR']
+    >>> jira_skip_tags('[SPARK-1234][SQL][FOLLOWUP] Fix')
+    ['FOLLOWUP']
+    >>> jira_skip_tags('[SPARK-1234][SQL] Fix')
+    []
+    """
     skip_jira_title_tags = ("MINOR", "TRIVIAL", "FOLLOWUP")
-    jira_ids = leading_jira_ids(title)
-    # Revert/Reapply PRs keep their title verbatim, so the tags are read 
inside the wrappers,
+    # Revert/Reapply PRs keep their title as is, so the tags are read inside 
the wrappers,
     # without Title.parse, which rejects some older titles such as 
[SPARK-N][FOLLOWUP][MINOR].
     tags = leading_title_tags(title)
-    if jira_ids:
+    if leading_jira_ids(title):
         # Title.parse rejects an id next to [MINOR] or [TRIVIAL], and 
prompt_for_components()
         # rejects both markers, so only a subject that predates the rule or 
was not merged with
         # this script has both. Such a commit was normally merged as the fix 
for that id, so
         # the id is updated.
         tags -= _LEADING_TAGS
-    skipped = [tag for tag in skip_jira_title_tags if tag in tags]
+    return [tag for tag in skip_jira_title_tags if tag in tags]
+
+
+def _bracketed(tags):
+    """Format tags as in a title, e.g. ``[MINOR], [FOLLOWUP]``, for JIRA 
messages."""
+    return ", ".join("[%s]" % tag for tag in tags)
+
+
+def jira_action(title, is_revert):
+    """Decide what :func:`update_jira_for_pr` does, as ``(action, detail)``.
+
+    ``action`` is ``"disabled"`` when JIRA is unreachable, ``"skip"`` when the 
title has
+    skip tags (``detail`` lists them), and otherwise ``"revert"`` or 
``"resolve"``, with
+    the leading JIRA ids as ``detail``; with no id, the committer enters one 
at the
+    prompt. :func:`describe_jira_action` shows the same decision before the 
push.
+
+    >>> jira_action('[SPARK-1234][SPARK-5678][SQL] Fix', False)
+    ('resolve', ['SPARK-1234', 'SPARK-5678'])
+    >>> jira_action('Revert "Fix"', True)
+    ('revert', [])
+    >>> jira_action('Revert "[MINOR][SQL] Fix"', True)
+    ('skip', ['MINOR'])
+    >>> from unittest.mock import patch
+    >>> with patch.dict(jira_action.__globals__, jira_disabled=True):
+    ...     jira_action('[SPARK-1234][SQL] Fix', False)
+    ('disabled', [])
+    """
+    if jira_disabled:
+        return "disabled", []
+    skipped = jira_skip_tags(title)
     if skipped:
+        return "skip", skipped
+    return ("revert" if is_revert else "resolve"), leading_jira_ids(title)
+
+
+def describe_jira_action(title, is_revert):
+    """Say what :func:`update_jira_for_pr` will do (see :func:`jira_action`), 
before a push.
+
+    Both JIRA paths leave a ticket that is resolved other than as Fixed as it 
is (see
+    :func:`get_jira_issue`), which the summary cannot tell without fetching 
the ticket,
+    so it says so.
+
+    >>> print(describe_jira_action('[SPARK-1234][SQL] Fix', False))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    resolve SPARK-1234 (or add fix versions if already resolved as Fixed;
+    a ticket resolved other than as Fixed is left as is)
+    >>> print(describe_jira_action('Revert "[SPARK-1234][SQL] Fix"', True))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    revert SPARK-1234 (remove fix versions and offer to reopen if resolved;
+    a ticket resolved other than as Fixed is left as is)
+    >>> print(describe_jira_action('Revert "Fix"', True))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    revert a ticket entered at the prompt (remove fix versions and offer to 
reopen if resolved;
+    a ticket resolved other than as Fixed is left as is)
+    >>> describe_jira_action('Revert "[MINOR][SQL] Fix"', True)
+    'skip (title has [MINOR])'
+    >>> from unittest.mock import patch
+    >>> with patch.dict(describe_jira_action.__globals__, jira_disabled=True):
+    ...     describe_jira_action('[SPARK-1234][SQL] Fix', False)
+    'skip (JIRA is unreachable)'
+    """
+    action, detail = jira_action(title, is_revert)
+    if action == "disabled":
+        return "skip (JIRA is unreachable)"
+    if action == "skip":
+        return "skip (title has %s)" % _bracketed(detail)
+    ids = ", ".join(detail) or "a ticket entered at the prompt"
+    other = "a ticket resolved other than as Fixed is left as is"
+    if action == "revert":
+        return "revert %s (remove fix versions and offer to reopen if 
resolved; %s)" % (ids, other)
+    return "resolve %s (or add fix versions if already resolved as Fixed; %s)" 
% (ids, other)
+
+
+def update_jira_for_pr(pr_num, title, merge_branches, title_components, *, 
is_revert):

Review Comment:
   Done, and the same for `title_components`, which is also determined by the 
title: `prepare_merge_title()` writes the normalized components, including any 
entered at the prompt, into the title it returns. `update_jira_for_pr(pr_num, 
title, merge_branches)` now reads the ids, the skip tags, the direction 
(`_is_net_revert` over the wrapper words), and the components from the title, 
and `prepare_merge_title()` returns only the title, as does the new 
`backport_jira_title()` (see [13]). On a merge, the reconciled components are 
the same as before for every title I tried. `jira_action()` and 
`describe_jira_action()` moved out with [20], so the summary line in that PR 
will derive it from the title too.
   



##########
dev/merge_spark_pr.py:
##########
@@ -1347,11 +1391,14 @@ def check_linked_jira_issues(pr_num, title):
     # reference the wrong ticket -- e.g. mistaking a GitHub PR number in the 
body
     # ("Closes #NNNNN") for a SPARK JIRA id -- which resolves an unrelated 
ticket on merge.
     # Show the PR title next to each ticket's summary, warn when a ticket is 
already
-    # Resolved/Closed (a fresh merge should target an open one), and require 
confirmation.
+    # Resolved/Closed (a fresh merge should target an open one, while a 
Revert/Reapply PR
+    # normally targets one resolved as Fixed, see format_jira_verification), 
and require
+    # confirmation.
     # For [FOLLOWUP] PRs the title describes the follow-up fix rather than the 
ticket, so
     # its low similarity to the JIRA summary is expected and the warning is 
suppressed.
     # The tag is read inside Revert/Reapply wrappers too.
     is_followup = "FOLLOWUP" in leading_title_tags(title)
+    is_wrapped = bool(_unwrap_wrappers(title)[1])

Review Comment:
   This moved to SPARK-60883 with [20], since only the warning change uses 
`is_wrapped`, and I'll handle it there. In this PR, after [17], every consumer 
reads the wrappers from the title through `_unwrap_wrappers()`, so there is one 
definition of the wrapper rules and no extra value threaded through `main()`.
   



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