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


##########
dev/merge_spark_pr.py:
##########
@@ -2658,61 +2876,7 @@ def main():
     if pr.get("draft", False) or "[WIP]" in title or "[DO-NOT-MERGE]" in title:
         fail("Cannot merge a draft PR #%s: %s" % (pr_num, title))
 
-    # e.g. 'Revert "[SPARK-56357][BUILD] Upgrade sbt to 1.12.8"'
-    is_revert_pr = title.startswith('Revert "') and title.endswith('"')
-    # e.g. 'Reapply "[SPARK-56357][BUILD] Upgrade sbt to 1.12.8"'
-    is_reapply_pr = title.startswith('Reapply "') and title.endswith('"')
-
-    # Normalized PR-title component tags, used later to reconcile JIRA 
components. Empty for
-    # Revert/Reapply PRs, whose titles are kept verbatim and not parsed for 
components.
-    title_components: List[str] = []
-
-    # Revert and Reapply PRs keep their title verbatim.
-    if not (is_revert_pr or is_reapply_pr):
-        # Parse; fail on a malformed title.
-        try:
-            parsed = Title.parse(title)
-        except ValueError as e:
-            fail("Malformed PR title: %s" % e)
-
-        # Normalize component tags via the registry and track primary.
-        components = []
-        has_primary = False
-        for tag in parsed.components:
-            c = Component.find(tag)
-            if c is not None and c.primary:
-                has_primary = True
-            components.append(Component.normalize(tag))
-        if not has_primary:
-            new_tags = prompt_for_components()
-            components = new_tags + components
-
-        # Deduplicate tags in insertion order.
-        components = list(dict.fromkeys(components))
-
-        # Move version tags (e.g. [4.X], [4.2]) to the head of components.
-        versions = [t for t in components if _VERSION_TAG_RE.match(t)]
-        if versions:
-            others = [t for t in components if not _VERSION_TAG_RE.match(t)]
-            components = versions + others
-
-        # Move FOLLOWUP to the last tag.
-        non_followup = [t for t in components if t != "FOLLOWUP"]
-        if len(non_followup) < len(components):
-            components = non_followup + ["FOLLOWUP"]
-
-        # Warn about tags that are neither known components nor version tags.
-        unknown = [
-            t for t in components if Component.find(t) is None and not 
_VERSION_TAG_RE.match(t)
-        ]
-        if unknown:
-            print_error("Title has unknown tag(s): %s" % ", ".join("[%s]" % t 
for t in unknown))
-
-        parsed.components = components
-        title_components = list(parsed.components)
-        title = str(parsed)
-        if title != pr["title"]:
-            print("Normalized title: %s" % title)
+    title = prepare_merge_title(title)

Review Comment:
   Done. `main()` now looks up the merge commit right after the draft check, 
and a new `title_for_run()` decides the title for the run. When the commit has 
the PR's merge footer, `committed_title()` returns its subject, and the 
backport uses it without calling `prepare_merge_title()`, so the current PR 
title is not passed to `Title.parse()` and no component is asked for; the ids, 
the direction, and the components come from the subject. A title without a 
primary component, e.g. `[SPARK-1][TESTS] Fix`, no longer prompts on such a 
backport, and one that `Title.parse()` rejects no longer stops it. 
`prepare_merge_title()` runs only for a merge and for the no-footer fallback, 
e.g. a squash-button commit, where it still prompts, and still fails on such a 
title, as before. `title_for_run()` has a doctest for the three cases, and the 
mocked run of `main()` checks that, on a backport, the component prompt appears 
only in the no-footer fallback.
   
   This replaces `backport_jira_title()`, which my replies to [13], [16], and 
[17] describe. My reply to [13] also said that a backport still prepares the PR 
title and stops on a title `Title.parse()` rejects, that 
`check_wrapped_title()` runs after the backport branch, and that a PR title 
corrected after the merge is not used; for a commit with the PR's merge footer, 
none of these holds now (see [24] and [26]). My reply to [16] listed the calls 
in `main()` that no doctest pins. The `prepare_merge_title()` and 
`check_wrapped_title()` calls moved into `title_for_run()`, whose doctest pins 
them, and the footer check moved into `jira_title_for_backport()`, which has 
one too. The `title_for_run()` and `jira_title_for_backport()` calls and both 
`update_jira_for_pr` calls are still in `main()`; the mocked run kills the four 
mutants there.
   



##########
dev/merge_spark_pr.py:
##########
@@ -670,6 +670,49 @@ def message_of(commit_hash):
     return None, None
 
 
+def backport_jira_title(pr_num, message: str, title: str) -> str:
+    """Return the title that the JIRA update of a backport run reads.
+
+    The JIRA update follows what was committed, so it reads the subject of the 
merge commit
+    ``message`` rather than the current PR title, which may have been edited 
since the
+    merge. A changed subject is printed next to the PR title. The subject is 
used only when
+    the merge footer of ``message`` closes ``pr_num``, i.e. when ``merge_pr`` 
made the
+    commit for this PR. Otherwise the PR title is used as before, e.g. for a 
commit made
+    with the GitHub squash button, whose subject ends with ``(#N)``.
+
+    >>> footer = "\\n\\nBody\\n\\nCloses #1 from a/b.\\n\\nAuthored-by: A 
<[email protected]>"
+    >>> backport_jira_title(1, 'Revert "[SPARK-1][SQL] Fix"' + footer,
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    'Revert "[SPARK-1][SQL] Fix"'
+    >>> backport_jira_title(1, 'Revert "Revert "[SPARK-1][SQL] Fix"" \\r' + 
footer,
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    ... # doctest: +NORMALIZE_WHITESPACE
+    The JIRA update uses the merge commit subject: Revert "Revert 
"[SPARK-1][SQL] Fix""
+    (current PR title: Revert "[SPARK-1][SQL] Fix"). If the PR title is the 
right one, update
+    JIRA by hand, answering N if asked to update it.
+    'Revert "Revert "[SPARK-1][SQL] Fix""'
+
+    A commit without this PR's merge footer, such as a squash-button merge or 
a commit of
+    another PR, leaves the PR title:
+
+    >>> backport_jira_title(1, 'Revert "[SPARK-1][SQL] Fix" (#1)\\n\\nBody',
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    'Revert "[SPARK-1][SQL] Fix"'
+    >>> backport_jira_title("1", "[SPARK-2][SQL] Other" + footer.replace("#1", 
"#2"),
+    ...                     "[SPARK-1][SQL] Fix")
+    '[SPARK-1][SQL] Fix'
+    """
+    subject = message.split("\n", 1)[0].strip()
+    if subject == title or not has_merge_footer(message, pr_num):

Review Comment:
   Done, with both parts. `jira_title_for_backport()` passes the PR title to 
`committed_title_notice()`, which takes it as GitHub has it, stripped, not the 
prepared one, and compares the two with `jira_action(title)`, the decision 
`update_jira_for_pr` now uses too: whether JIRA is skipped, and if not, which 
tickets and in which direction. So a difference in components, text, or 
normalization gives no notice, e.g. for 65396d2f8d2 with `[TESTS]`, or 
`[SPARK-1][CORE] Fix` merged after `CORE` was entered at the prompt. Neither do 
two titles that both skip JIRA, or that list the same ids in another order. 
Since the PR title is no longer prepared on that path (see [22]), the notice 
cannot show a title built at this run's prompt either. When the notice does 
fire, the committer can choose the PR title for the JIRA update (see [24]).
   
   This updates my replies to [10] (both), [17], [19], and [20]: 
`jira_action()` is back in this PR, now as `jira_action(title)`, without 
`is_revert` or a disabled case, which the callers check. It is the one place 
that decides skip, ids, and direction for both the notice and the update, so 
the skip logic, with the skip-tag comment from [4], is no longer inline in 
`update_jira_for_pr`; `_bracketed()` is back as well, for the skip message, the 
notice, and the two tag messages in `prepare_merge_title()` and the component 
prompt. The pre-merge `jira` line stays in SPARK-60883. Within one call it 
computes the ids once; on a backport, the subject goes through it twice, once 
for the notice and once in `update_jira_for_pr`, which I kept so that 
`update_jira_for_pr` still reads only its title ([17]).
   



##########
dev/merge_spark_pr.py:
##########
@@ -670,6 +670,49 @@ def message_of(commit_hash):
     return None, None
 
 
+def backport_jira_title(pr_num, message: str, title: str) -> str:
+    """Return the title that the JIRA update of a backport run reads.
+
+    The JIRA update follows what was committed, so it reads the subject of the 
merge commit
+    ``message`` rather than the current PR title, which may have been edited 
since the
+    merge. A changed subject is printed next to the PR title. The subject is 
used only when
+    the merge footer of ``message`` closes ``pr_num``, i.e. when ``merge_pr`` 
made the
+    commit for this PR. Otherwise the PR title is used as before, e.g. for a 
commit made
+    with the GitHub squash button, whose subject ends with ``(#N)``.
+
+    >>> footer = "\\n\\nBody\\n\\nCloses #1 from a/b.\\n\\nAuthored-by: A 
<[email protected]>"
+    >>> backport_jira_title(1, 'Revert "[SPARK-1][SQL] Fix"' + footer,
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    'Revert "[SPARK-1][SQL] Fix"'
+    >>> backport_jira_title(1, 'Revert "Revert "[SPARK-1][SQL] Fix"" \\r' + 
footer,
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    ... # doctest: +NORMALIZE_WHITESPACE
+    The JIRA update uses the merge commit subject: Revert "Revert 
"[SPARK-1][SQL] Fix""
+    (current PR title: Revert "[SPARK-1][SQL] Fix"). If the PR title is the 
right one, update
+    JIRA by hand, answering N if asked to update it.
+    'Revert "Revert "[SPARK-1][SQL] Fix""'
+
+    A commit without this PR's merge footer, such as a squash-button merge or 
a commit of
+    another PR, leaves the PR title:
+
+    >>> backport_jira_title(1, 'Revert "[SPARK-1][SQL] Fix" (#1)\\n\\nBody',
+    ...                     'Revert "[SPARK-1][SQL] Fix"')
+    'Revert "[SPARK-1][SQL] Fix"'
+    >>> backport_jira_title("1", "[SPARK-2][SQL] Other" + footer.replace("#1", 
"#2"),
+    ...                     "[SPARK-1][SQL] Fix")
+    '[SPARK-1][SQL] Fix'
+    """
+    subject = message.split("\n", 1)[0].strip()
+    if subject == title or not has_merge_footer(message, pr_num):
+        return title
+    print(

Review Comment:
   Done. The notice is now printed in the `finally`, right before 
`update_jira_for_pr`, after the cherry-picks, and it no longer sends the 
committer to N. It shows each title and what it would do, e.g. `resolve 
SPARK-1, or add fix versions to it` against `remove fix versions from SPARK-1, 
and offer to reopen it if resolved`, notes that a ticket resolved other than as 
Fixed is left as is either way, and then asks `Update JIRA from the current PR 
title instead of the merge commit subject? (y/N)` 
(`jira_title_for_backport()`). The default keeps the subject; y uses the PR 
title as is, apart from surrounding whitespace, so a wrong id fixed in the 
title after the merge can still reach JIRA. It is shown only on a backport of a 
commit with the PR's merge footer, since otherwise there is no committed 
subject to compare with; `jira_title_for_backport()` checks that itself. When 
JIRA is disabled for the run, the notice is printed without the question, so 
the committer knows which title to recon
 cile by hand.
   
   Answering N at `Would you like to update an associated JIRA?` still ends the 
run with an error, as on master. That prompt is shared with the merge path, and 
nothing points the committer to N anymore, so I left it as is. The `Pull 
request ... has already been merged` and `Found commit ...` lines, and the 
check that the commit is local, also moved up, to right after the lookup, so a 
backport is announced, and a missing commit fails, before the body prompt. This 
updates my reply to [13]: the notice quoted there is gone, and a corrected PR 
title can now be chosen at the new question instead of updating JIRA by hand.
   



##########
dev/merge_spark_pr.py:
##########
@@ -2446,6 +2568,102 @@ def prompt_for_components():
         return components
 
 
+def prepare_merge_title(raw_title: str) -> str:
+    """Return the title to merge for the PR title ``raw_title``.
+
+    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 
(see
+    :func:`check_wrapped_title` for what the merge path checks in it). 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, and written back into the title, from which 
:func:`update_jira_for_pr` reads
+    them. A title that changes is printed.
+    """
+    title = raw_title.strip()
+    if not _unwrap_wrappers(title)[1]:
+        # Parse; fail on a malformed title.
+        try:
+            parsed = Title.parse(title)
+        except ValueError as e:
+            fail("Malformed PR title: %s" % e)
+
+        # Normalize component tags via the registry and track primary.
+        components = []
+        has_primary = False
+        for tag in parsed.components:
+            c = Component.find(tag)
+            if c is not None and c.primary:
+                has_primary = True
+            components.append(Component.normalize(tag))
+        if not has_primary:
+            new_tags = prompt_for_components()
+            components = new_tags + components
+
+        # Deduplicate tags in insertion order.
+        components = list(dict.fromkeys(components))
+
+        # Move version tags (e.g. [4.X], [4.2]) to the head of components.
+        versions = [t for t in components if _VERSION_TAG_RE.match(t)]
+        if versions:
+            others = [t for t in components if not _VERSION_TAG_RE.match(t)]
+            components = versions + others
+
+        # Move FOLLOWUP to the last tag.
+        non_followup = [t for t in components if t != "FOLLOWUP"]
+        if len(non_followup) < len(components):
+            components = non_followup + ["FOLLOWUP"]
+
+        # Warn about tags that are neither known components nor version tags.
+        unknown = [
+            t for t in components if Component.find(t) is None and not 
_VERSION_TAG_RE.match(t)
+        ]
+        if unknown:
+            print_error("Title has unknown tag(s): %s" % ", ".join("[%s]" % t 
for t in unknown))
+
+        parsed.components = components
+        title = str(parsed)
+    if title != raw_title:
+        print("Normalized title: %s" % title)
+    return title
+
+
+def check_wrapped_title(title: str) -> None:
+    """Check the ``Revert "..."`` / ``Reapply "..."`` wrappers of a PR title 
before merging.
+
+    An empty innermost title fails, since the commit subject would say 
nothing. An
+    innermost title that still starts with ``Revert`` or ``Reapply``, or a 
form of either
+    such as ``Reverting`` (see ``_WRAPPER_WORD_RE``), is warned about and 
needs a
+    confirmation. Such a title may have a dropped quote, e.g. ``Revert "Revert 
"[SPARK-N]
+    ..."``, or wrap a subject that is not a wrapper, e.g. ``Revert to using 
...``. The
+    shape does not tell which: the revert of an existing subject that GitHub 
truncated
+    without its closing quote looks like a dropped quote. Either way, only the 
outer
+    wrappers decide the direction, and no JIRA id or skip tag is read from it. 
Other
+    titles pass silently.
+    """
+    inner, words = _unwrap_wrappers(title)
+    if not words:
+        return
+    if not inner:
+        fail("Empty title inside the Revert/Reapply wrappers of the PR title: 
%s" % title)
+    if _WRAPPER_WORD_RE.match(inner):

Review Comment:
   Done, with your rule. `check_wrapped_title()` also warns, with the same 
confirmation, when the innermost title starts with `"`, or with one word of 
letters or hyphens followed by `"`, with at most one space between 
(`_WRAPPER_LIKE_RE`). This covers `Revert ""[SPARK-1][SQL] X""`, `Revert 
"Reaply "[SPARK-1][SQL] X""`, and `Revert "Reaply"[SPARK-1][SQL] X""`; the 
doctest covers these shapes, `Reapply "Revet "...""`, and which of the two 
warnings each title gets. A quote later in the title, e.g. `Revert 
"[SPARK-1][SQL] Support "foo" in X"`, is not warned about.
   
   The rule also catches a subject that starts with a quoted word or quotes one 
after its first word. No master subject itself is warned about, but reverting 
any of 21 existing subjects, e.g. `Rename "jobs" to "applications" in the 
standalone cluster`, now asks for a confirmation, and the warning tells the 
committer to keep such a title. Requiring the innermost title to also end with 
`"` would cut that to 3, but would miss a misspelled wrapper whose closing 
quote was dropped, so I kept your version. A misspelled wrapper that also lost 
its quote, e.g. `Revert "Reaply [SPARK-1][SQL] X"`, is still not caught, since 
nothing in it looks like a wrapper. The warning names the shape that matched, a 
leading quote or one word and a quote. When JIRA is disabled for the run, both 
warnings say what a JIRA update would do and to reconcile the ticket by hand, 
instead of pointing to a JIRA prompt that does not come.
   



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