tools.linux.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Mark Brown <broonie@kernel.org>
To: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
Cc: tools@kernel.org, Mark Brown <broonie@kernel.org>
Subject: [PATCH] review: Generate thanks commit URLs for known repositories automatically
Date: Thu, 10 Sep 2026 12:32:26 +0100	[thread overview]
Message-ID: <20260910113504.84582-1-broonie@kernel.org> (raw)

At present generating links to commits in web interfaces for inclusion in
thanks mails requires manual configuration of a global configuration option
b4.thanks-commit-url-mask, or the per remote equivalent b4-thanks-url-mask.
Given that we also have the b4-check-repo option to configure where to check
to see if thanks are ready to be sent and practically speaking this is
likely to be one of a very small set of hosting providers this is all a bit
redundant.

If one of git.kernel.org, github or gitlab is configured for the check repo
then default to generating a link based on our knowledge of the remote, a
manual configuration will still override the default.

Signed-off-by: Mark Brown <broonie@kernel.org>
---
 docs/config.rst                             |  29 ++++-
 docs/maintainer/review.rst                  |  15 +--
 src/b4/command.py                           |   4 +
 src/b4/review_tui/_tracking_app.py          |   4 +-
 src/b4/templates/thanks-am-template.example |   3 +
 src/b4/templates/thanks-pr-template.example |   3 +
 src/b4/ty.py                                | 116 +++++++++++++++-----
 src/tests/test_tui_tracking.py              |  45 ++++++++
 src/tests/test_ty.py                        |  15 +++
 9 files changed, 190 insertions(+), 44 deletions(-)

diff --git a/docs/config.rst b/docs/config.rst
index 4dce6892..9b7da54c 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -463,6 +463,11 @@ Attestation settings
          [remote "spi"]
              b4-check-repo = https://git.kernel.org/pub/scm/linux/kernel/git/username/spi.git
 
+     For recognized HTTP(S) web hosts (currently git.kernel.org,
+     github.com, and gitlab.com), b4 also uses this URL to link commits in
+     the thank-you message. Other transports and hosting layouts can use
+     the per-remote ``b4-commit-url-mask`` described below.
+
      See :ref:`queuing thanks <thanks_queue>` for details.
 
      Default: ``None``
@@ -471,14 +476,26 @@ Attestation settings
      Used when creating summaries for ``b4 ty`` and the review TUI
      thank-you flow. Can be a value like::
 
-         thanks-commit-url-mask = https://git.kernel.org/username/c/%.12s
+         thanks-commit-url-mask = https://git.kernel.org/username/tree/c/%.12s
+
+     If no explicit mask or recognized check-repository URL is available,
+     b4 falls back to using commit hashes.
+
+     For repositories with multiple remotes, an explicit per-remote mask
+     takes precedence over this setting::
+
+         [remote "spi"]
+             b4-commit-url-mask = https://git.kernel.org/username/spi/c/%.12s
 
-     If not set, b4 falls back to using commit hashes.
+     When no explicit per-remote mask is set, b4 automatically derives one
+     from that remote's ``b4-check-repo`` for recognized HTTP(S) web hosts.
+     The same derivation is attempted from :term:`b4.thanks-check-repo` when
+     neither a per-remote nor this global mask is available.
 
-     When this option is set, the review TUI's thank-you preview offers
-     a **Queue** option that stores the message for delayed delivery.
-     Queued messages are held until their commit is verified published,
-     so you can push your commits before the thank-you is sent.
+     When a commit URL can be generated, the review TUI's thank-you preview
+     offers a **Queue** option that stores the message for delayed delivery.
+     Queued messages are held until their commit is verified published, so
+     you can push your commits before the thank-you is sent.
      See :ref:`queuing thanks <thanks_queue>` for details.
 
      .. note::
diff --git a/docs/maintainer/review.rst b/docs/maintainer/review.rst
index 47aa8e7b..1b47780f 100644
--- a/docs/maintainer/review.rst
+++ b/docs/maintainer/review.rst
@@ -1023,13 +1023,14 @@ screen shows the generated message with keybindings to **Send**
 
 **Queuing thanks for delayed delivery**
 
-When :term:`b4.thanks-commit-url-mask` is configured, the thank-you
-preview also offers a **Queue** option (``W``). Queuing writes the
-message as an RFC 2822 file in ``.git/b4-review/queue/`` instead of
-sending it immediately. This is useful when you want to push your
-commits to a public tree before sending thank-you messages — the
-message references commit URLs that may not resolve until the push is
-complete.
+When b4 can generate a web link for the commit — from
+:term:`b4.thanks-commit-url-mask`, a per-remote ``b4-commit-url-mask``,
+or a recognized HTTP(S) ``b4-check-repo`` URL — the thank-you preview also
+offers a **Queue** option (``W``). Queuing writes the message as an RFC 2822
+file in ``.git/b4-review/queue/`` instead of sending it immediately. This
+is useful when you want to push your commits to a public tree before
+sending thank-you messages — the message references commit URLs that may
+not resolve until the push is complete.
 
 Files are named ``{change-id}-v{revision}.msg`` and the check target is
 recorded as ``X-Check-URL``, ``X-Check-Commit``, ``X-Check-Repo`` and
diff --git a/src/b4/command.py b/src/b4/command.py
index 00c4c84b..41bb9ae8 100644
--- a/src/b4/command.py
+++ b/src/b4/command.py
@@ -249,6 +249,10 @@ def cmd_pr(cmdargs: argparse.Namespace) -> None:
 def cmd_ty(cmdargs: argparse.Namespace) -> None:
     import b4.ty
 
+    if cmdargs.gitdir:
+        # The initial setup happens relative to cwd. Reload repository-local
+        # thank-you settings when -g names a different tree.
+        b4.setup_config(cmdargs, topdir=cmdargs.gitdir)
     b4.ty.main(cmdargs)
 
 
diff --git a/src/b4/review_tui/_tracking_app.py b/src/b4/review_tui/_tracking_app.py
index 4c25f8cf..0f39b8f2 100644
--- a/src/b4/review_tui/_tracking_app.py
+++ b/src/b4/review_tui/_tracking_app.py
@@ -4926,8 +4926,8 @@ class TrackingApp(LoreNodeShutdownMixin, CheckRunnerMixin, App[Optional[str]]):
         # Compute checkurl from last taken commit for queue support
         checkurl: Optional[str] = None
         checkcommit: Optional[str] = None
-        cidmask = config.get('thanks-commit-url-mask')
-        if isinstance(cidmask, str) and cidmask and '%' in cidmask:
+        cidmask = b4.ty.get_commit_url_mask(topdir, target_branch)
+        if cidmask and '%' in cidmask:
             # Find the last commit ID (highest patch index with a commit)
             last_cid: Optional[str] = None
             for _idx, cid in commits:
diff --git a/src/b4/templates/thanks-am-template.example b/src/b4/templates/thanks-am-template.example
index b2c8986a..bac889a0 100644
--- a/src/b4/templates/thanks-am-template.example
+++ b/src/b4/templates/thanks-am-template.example
@@ -20,6 +20,9 @@ ${quote}
 #   url = https://...
 #   fetch = ...
 #   b4-treename = uname/sound.git
+#   b4-check-repo = https://git.kernel.org/pub/scm/linux/kernel/git/uname/sound.git
+# The check repo is also used to generate commit links for known web hosts.
+# For other hosts, or to choose a different link format, set an explicit mask:
 #   b4-commit-url-mask = https://git.kernel.org/uname/sound/c/%.8s
 Applied to ${branch}, thanks!
 
diff --git a/src/b4/templates/thanks-pr-template.example b/src/b4/templates/thanks-pr-template.example
index 13bd2058..afae4992 100644
--- a/src/b4/templates/thanks-pr-template.example
+++ b/src/b4/templates/thanks-pr-template.example
@@ -20,6 +20,9 @@ ${quote}
 #   url = https://...
 #   fetch = ...
 #   b4-treename = uname/sound.git
+#   b4-check-repo = https://git.kernel.org/pub/scm/linux/kernel/git/uname/sound.git
+# The check repo is also used to generate commit links for known web hosts.
+# For other hosts, or to choose a different link format, set an explicit mask:
 #   b4-commit-url-mask = https://git.kernel.org/uname/sound/c/%.8s
 Merged into ${branch}, thanks!
 
diff --git a/src/b4/ty.py b/src/b4/ty.py
index bbce1499..a6d9ad9b 100644
--- a/src/b4/ty.py
+++ b/src/b4/ty.py
@@ -13,6 +13,7 @@ import json
 import os
 import re
 import sys
+import urllib.parse
 from email.message import EmailMessage
 from pathlib import Path
 from string import Template
@@ -61,8 +62,6 @@ ${signature}
 
 # Used to track commits created by current user
 MY_COMMITS: Optional[Dict[str, Tuple[str, str, List[str]]]] = None
-# Used to track additional branch info
-BRANCH_INFO: Optional[Dict[str, str]] = None
 
 
 def git_get_merge_id(
@@ -294,6 +293,72 @@ def auto_locate_series(
     return found
 
 
+def _derive_commit_url_mask(repo: str) -> Optional[str]:
+    """Return a commit URL mask for known web repository layouts."""
+    try:
+        parsed = urllib.parse.urlparse(repo.rstrip('/'))
+        hostname = parsed.hostname
+        has_credentials = parsed.username is not None or parsed.password is not None
+    except ValueError:
+        return None
+    if parsed.scheme not in ('http', 'https') or not hostname:
+        return None
+    if has_credentials or parsed.query or parsed.fragment:
+        return None
+
+    hostname = hostname.lower()
+    path = parsed.path.rstrip('/')
+    if hostname in ('github.com', 'www.github.com', 'gitlab.com', 'www.gitlab.com'):
+        path = re.sub(r'\.git$', '', path)
+    if hostname == 'git.kernel.org':
+        repo_parts = re.sub(r'\.git$', '', path).strip('/').split('/')
+        if len(repo_parts) < 2 or not all(repo_parts):
+            return None
+        short_path = f'/{repo_parts[-2]}/{repo_parts[-1]}'
+        base = urllib.parse.urlunparse(
+            (parsed.scheme, parsed.netloc, short_path, '', '', '')
+        ).replace('%', '%%')
+        return f'{base}/c/%.12s'
+    base = urllib.parse.urlunparse(
+        (parsed.scheme, parsed.netloc, path, '', '', '')
+    ).replace('%', '%%')
+    if hostname in ('github.com', 'www.github.com'):
+        return f'{base}/commit/%s'
+    if hostname in ('gitlab.com', 'www.gitlab.com'):
+        return f'{base}/-/commit/%s'
+    return None
+
+
+def _get_commit_url_mask(config: ConfigDictT, binfo: Dict[str, str]) -> Optional[str]:
+    """Resolve the commit URL mask for one branch's remote."""
+    if 'b4-commit-url-mask' in binfo:
+        return binfo['b4-commit-url-mask'] or None
+
+    checkrepo = binfo.get('b4-check-repo')
+    if checkrepo:
+        cidmask = _derive_commit_url_mask(checkrepo)
+        if cidmask:
+            return cidmask
+
+    global_cidmask = config.get('thanks-commit-url-mask')
+    if global_cidmask:
+        assert isinstance(global_cidmask, str), (
+            'thanks-commit-url-mask must be a string'
+        )
+        return global_cidmask
+
+    global_checkrepo = config.get('thanks-check-repo')
+    if global_checkrepo:
+        assert isinstance(global_checkrepo, str), 'thanks-check-repo must be a string'
+        return _derive_commit_url_mask(global_checkrepo)
+    return None
+
+
+def get_commit_url_mask(gitdir: Optional[str], branch: str) -> Optional[str]:
+    """Return the effective commit URL mask for a branch."""
+    return _get_commit_url_mask(b4.get_main_config(), get_branch_info(gitdir, branch))
+
+
 def set_branch_details(
     gitdir: Optional[str], branch: str, jsondata: JsonDictT, config: ConfigDictT
 ) -> Tuple[JsonDictT, ConfigDictT]:
@@ -302,8 +367,6 @@ def set_branch_details(
     for key, val in binfo.items():
         if key == 'b4-treename':
             config['thanks-treename'] = val
-        elif key == 'b4-commit-url-mask':
-            config['thanks-commit-url-mask'] = val
         elif key == 'b4-pr-template':
             config['thanks-pr-template'] = val
         elif key == 'b4-am-template':
@@ -311,6 +374,8 @@ def set_branch_details(
         elif key == 'branch':
             jsondata['branch'] = val
 
+    config['thanks-commit-url-mask'] = _get_commit_url_mask(config, binfo)
+
     if 'thanks-treename' in config and isinstance(config['thanks-treename'], str):
         jsondata['treename'] = config['thanks-treename']
     elif 'url' in binfo:
@@ -330,7 +395,7 @@ def set_branch_details(
 def generate_pr_thanks(
     gitdir: Optional[str], jsondata: JsonDictT, branch: str, cmdargs: argparse.Namespace
 ) -> EmailMessage:
-    config = b4.get_main_config()
+    config = b4.get_main_config().copy()
     jsondata, config = set_branch_details(gitdir, branch, jsondata, config)
     thanks_template = DEFAULT_PR_TEMPLATE
     _ctpr = config.get('thanks-pr-template')
@@ -369,9 +434,7 @@ def generate_pr_thanks(
 def generate_am_thanks(
     gitdir: Optional[str], jsondata: JsonDictT, branch: str, cmdargs: argparse.Namespace
 ) -> EmailMessage:
-    global BRANCH_INFO
-    BRANCH_INFO = None
-    config = b4.get_main_config()
+    config = b4.get_main_config().copy()
     jsondata, config = set_branch_details(gitdir, branch, jsondata, config)
     thanks_template = DEFAULT_AM_TEMPLATE
     _ctat = config.get('thanks-am-template')
@@ -900,7 +963,6 @@ def check_stale_thanks(outdir: str) -> None:
 
 
 def get_wanted_branch(cmdargs: argparse.Namespace) -> str:
-    global BRANCH_INFO
     gitdir = cmdargs.gitdir
     if not cmdargs.branch:
         # Find out our current branch
@@ -995,7 +1057,7 @@ def _parse_checkurl(checkurl: str) -> Tuple[Optional[str], Optional[str]]:
     """Extract (repo_url, commit_id) from a thanks-commit-url-mask URL.
 
     The repo URL is only derivable from cgit/github/gitlab-style commit
-    URLs; for anything else (e.g. git.kernel.org /username/c/ shortlinks)
+    URLs; for anything else (e.g. git.kernel.org /username/tree/c/ shortlinks)
     we can usually still recover the commit id from a trailing hex run.
     Either element may be None.
     """
@@ -1037,10 +1099,6 @@ def get_check_repo_for_branch(
     3. the branch's remote URL
     4. a repo URL derived from the commit check URL
     """
-    # Long-lived callers (the TUI) thank series on different branches;
-    # never serve another branch's cached remote info
-    global BRANCH_INFO
-    BRANCH_INFO = None
     binfo = get_branch_info(gitdir, branch)
     crepo = binfo.get('b4-check-repo')
     if crepo:
@@ -1568,20 +1626,18 @@ def _process_queue_locked(
 
 
 def get_branch_info(gitdir: Optional[str], branch: str) -> Dict[str, str]:
-    global BRANCH_INFO
-    if BRANCH_INFO is not None:
-        return BRANCH_INFO
-
-    BRANCH_INFO = dict()
+    branch_info: Dict[str, str] = dict()
 
-    remotecfg = b4.get_config_from_git('branch\\.%s\\..*' % branch)
+    remotecfg = b4.get_config_from_git(
+        r'^branch\.%s\.' % re.escape(branch), gitdir=gitdir
+    )
     if 'remote' not in remotecfg:
         # Did not find a matching branch entry, so look at remotes
         gitargs = ['remote', 'show']
         lines = b4.git_get_command_lines(gitdir, gitargs)
         if not len(lines):
             # No remotes? Hmm...
-            return BRANCH_INFO
+            return branch_info
 
         remote = None
         for entry in lines:
@@ -1591,21 +1647,23 @@ def get_branch_info(gitdir: Optional[str], branch: str) -> Dict[str, str]:
 
         if remote is None:
             # Not found any matching remotes
-            return BRANCH_INFO
+            return branch_info
 
-        BRANCH_INFO['remote'] = remote
-        BRANCH_INFO['branch'] = branch.replace(f'{remote}/', '')
+        branch_info['remote'] = remote
+        branch_info['branch'] = branch.replace(f'{remote}/', '')
 
     else:
-        BRANCH_INFO['remote'] = remotecfg['remote']
+        branch_info['remote'] = remotecfg['remote']
         if 'merge' in remotecfg:
-            BRANCH_INFO['branch'] = re.sub(r'^refs/heads/', '', remotecfg['merge'])
+            branch_info['branch'] = re.sub(r'^refs/heads/', '', remotecfg['merge'])
 
     # Grab template overrides
-    remotecfg = b4.get_config_from_git('remote\\.%s\\..*' % BRANCH_INFO['remote'])
-    BRANCH_INFO.update(remotecfg)
+    remotecfg = b4.get_config_from_git(
+        r'^remote\.%s\.' % re.escape(branch_info['remote']), gitdir=gitdir
+    )
+    branch_info.update(remotecfg)
 
-    return BRANCH_INFO
+    return branch_info
 
 
 def main(cmdargs: argparse.Namespace) -> None:
diff --git a/src/tests/test_tui_tracking.py b/src/tests/test_tui_tracking.py
index e3d79ef7..1e064cfd 100644
--- a/src/tests/test_tui_tracking.py
+++ b/src/tests/test_tui_tracking.py
@@ -2931,6 +2931,51 @@ class TestSeriesLifecycle:
         # Only the two taken patches contribute commits (by 1-based index).
         assert jsondata['commits'] == [(1, 'aaa111'), (2, 'bbb222')]
 
+    def test_thank_preview_uses_branch_check_repo_url(self, gitdir: str) -> None:
+        """The queue target uses the same per-remote URL as the message."""
+        from unittest import mock
+
+        checkrepo = 'https://git.kernel.org/pub/scm/linux/kernel/git/example/spi.git'
+        b4.git_set_config(gitdir, 'branch.for-next.remote', 'spi')
+        b4.git_set_config(gitdir, 'branch.for-next.merge', 'refs/heads/for-next')
+        b4.git_set_config(gitdir, 'remote.spi.b4-check-repo', checkrepo)
+        commit = '0123456789abcdef0123456789abcdef01234567'
+        tracking_data: Dict[str, Any] = {
+            'series': {
+                'header-info': {},
+                'taken': {'branch': 'for-next'},
+                'expected': 1,
+            },
+            'patches': [
+                {
+                    'title': '[PATCH] test patch',
+                    'header-info': {'msgid': 'patch@example.com'},
+                    'taken': {'commit-id': commit},
+                }
+            ],
+        }
+        series = {'change_id': 'test-change-id'}
+        app = mock.Mock()
+        generated = email.message.EmailMessage()
+
+        with (
+            patch('b4.git_get_toplevel', return_value=gitdir),
+            patch('b4.review.load_tracking', return_value=('cover', tracking_data)),
+            patch('b4.ty.generate_am_thanks', return_value=generated),
+            patch('b4.get_email_signature', return_value='Test'),
+        ):
+            TrackingApp._start_thank(app, series)
+
+        app._show_thank_preview.assert_called_once_with(
+            generated,
+            series,
+            checkurl=f'https://git.kernel.org/example/spi/c/{commit[:12]}',
+            checkcommit=commit,
+            archive_after=False,
+            checkrepo=checkrepo,
+            checkbranch='for-next',
+        )
+
     def test_partial_series_ingests_new_revision(self, gitdir: str) -> None:
         """A 'partial' series must ingest an incoming v2 and record it.
 
diff --git a/src/tests/test_ty.py b/src/tests/test_ty.py
index be132ff5..dbd6657f 100644
--- a/src/tests/test_ty.py
+++ b/src/tests/test_ty.py
@@ -7,6 +7,7 @@ from unittest import mock
 import pytest
 
 import b4
+import b4.command
 import b4.ty
 
 
@@ -62,6 +63,20 @@ def test_render_ty_review_layout() -> None:
     assert b4.ty.parse_ty_review(buf, sections) == set()
 
 
+def test_cmd_ty_reloads_config_for_gitdir(monkeypatch: pytest.MonkeyPatch) -> None:
+    """-g makes repository-local thank-you settings come from that tree."""
+    cmdargs = mock.Mock(gitdir='/other/repo')
+    setup_config = mock.Mock()
+    ty_main = mock.Mock()
+    monkeypatch.setattr(b4, 'setup_config', setup_config)
+    monkeypatch.setattr(b4.ty, 'main', ty_main)
+
+    b4.command.cmd_ty(cmdargs)
+
+    setup_config.assert_called_once_with(cmdargs, topdir='/other/repo')
+    ty_main.assert_called_once_with(cmdargs)
+
+
 def test_parse_ty_review_marks_skips() -> None:
     """Flipping '+' to 'x' on an item marks it (by position) skipped."""
     sections = _review_sections()
-- 
2.47.3


                 reply	other threads:[~2026-09-10 11:41 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910113504.84582-1-broonie@kernel.org \
    --to=broonie@kernel.org \
    --cc=konstantin@linuxfoundation.org \
    --cc=tools@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).