All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 00/13] b4: add native Windows support
@ 2026-04-25 19:49 Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 01/13] tests: specify UTF-8 encoding when opening files in text mode Adrian Neftali Sanchez
                   ` (12 more replies)
  0 siblings, 13 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Hello everyone,

This series aims to provide native Windows support for b4. 

While Linux remains the primary environment for kernel development, many 
contributors find themselves on Windows systems due to corporate 
policies, hardware constraints, or environments where WSL and virtual 
machines are restricted or unavailable. Enabling b4 to run natively 
allows these contributors to participate in patch review and mailing 
list workflows more efficiently.

The changes focus on platform-neutral path handling, encoding 
consistency, and process management. Key highlights:

- Portability: Replaced POSIX-specific assumptions (UID/GID, path 
  separators, and shell execution) with cross-platform equivalents.
- Stability: Fixed race conditions in the TUI worker and added 
  defensive guards for widget mounting, which became more apparent 
  under Windows' asynchronous I/O model (IocpProactor).
- Testing: Added ci.ps1 to allow local validation of the full 
  quality-gate pipeline on Windows.

The entire test suite (900+ tests) was validated on Windows 11. While 
some minor UI timing flakiness persists in the test environment due to 
console latency, the core functionality is stable and ready for use.

I am open to maintaining the Windows-specific execution paths and 
addressing any regressions that may arise on this platform.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>

Adrian Neftali Sanchez (13):
  tests: specify UTF-8 encoding when opening files in text mode
  tests: use os.path.join/normpath for portable path assertions
  b4: add cross-platform username resolution in _setup_user_config
  b4: resolve platform-native data and cache directories
  b4: normalise git-reported absolute paths with os.path.normpath
  b4: select platform-appropriate default pager
  ez: use portable uid/gid accessors in write_to_tar
  tui: The _suspend_to_shell function assumes a POSIX environment,
    relying on $SHELL and shell-specific arguments (e.g., --rcfile) that
    are invalid on Windows.
  tests: replace NamedTemporaryFile with tmp_path in patatt fixture
  pyproject: add Windows operating-system classifier
  ci: add ci.ps1 for running the quality-gate pipeline on Windows
  review_tui: write git tracking commit before updating DB
  review_tui: guard against unmounted diff-viewer In certain
    asynchronous contexts or during rapid UI transitions, _show_content
    may be invoked before the diff-viewer widget has been fully mounted
    in the DOM. This is particularly reproducible in headless test
    environments and on platforms with slower console I/O
    initialization.

 ci.ps1                             | 15 ++++++++
 pyproject.toml                     |  1 +
 src/b4/__init__.py                 | 22 +++++++++---
 src/b4/ez.py                       |  4 +--
 src/b4/review_tui/_review_app.py   |  8 ++++-
 src/b4/review_tui/_tracking_app.py | 24 ++++++++-----
 src/b4/tui/_common.py              | 58 ++++++++++++++++--------------
 src/tests/test___init__.py         |  2 +-
 src/tests/test_patatt.py           | 31 ++++++++++------
 src/tests/test_review_tracking.py  |  2 +-
 src/tests/test_three_way_merge.py  |  2 +-
 11 files changed, 113 insertions(+), 56 deletions(-)
 create mode 100644 ci.ps1

-- 
2.45.0.windows.1


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [RFC PATCH 01/13] tests: specify UTF-8 encoding when opening files in text mode
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 02/13] tests: use os.path.join/normpath for portable path assertions Adrian Neftali Sanchez
                   ` (11 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Python 3 open() inherits the locale encoding by default; on Windows that
is typically cp1252 rather than UTF-8.  Explicitly passing
encoding="utf-8" makes the test portable across platforms.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/tests/test___init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/tests/test___init__.py b/src/tests/test___init__.py
index 3c4c2d0..5544978 100644
--- a/src/tests/test___init__.py
+++ b/src/tests/test___init__.py
@@ -75,7 +75,7 @@ def test_save_git_am_mbox(
     dest = os.path.join(tmp_path, 'out')
     with open(dest, 'wb') as fh:
         b4.save_git_am_mbox(msgs, fh)
-    with open(dest, 'r') as fh:
+    with open(dest, 'r', encoding='utf-8') as fh:
         res = fh.read()
     assert re.search(regex, res, flags=flags)
 
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 02/13] tests: use os.path.join/normpath for portable path assertions
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 01/13] tests: specify UTF-8 encoding when opening files in text mode Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 03/13] b4: add cross-platform username resolution in _setup_user_config Adrian Neftali Sanchez
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Hardcoded forward-slash separators in path suffix checks fail on
Windows, where os.sep is a backslash.  Replace the literal string
comparisons with os.path.join and os.path.normpath so the assertions
hold on all platforms.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/tests/test_review_tracking.py | 2 +-
 src/tests/test_three_way_merge.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/tests/test_review_tracking.py b/src/tests/test_review_tracking.py
index 290c991..7d34951 100644
--- a/src/tests/test_review_tracking.py
+++ b/src/tests/test_review_tracking.py
@@ -23,7 +23,7 @@ class TestGetReviewDataDir:
         """Verify the review data directory is created."""
         reviewdir = review_tracking.get_review_data_dir()
         assert os.path.isdir(reviewdir)
-        assert reviewdir.endswith('b4/review')
+        assert reviewdir.endswith(os.path.join('b4', 'review'))
 
 
 class TestDbOperations:
diff --git a/src/tests/test_three_way_merge.py b/src/tests/test_three_way_merge.py
index 2b4391e..9982075 100644
--- a/src/tests/test_three_way_merge.py
+++ b/src/tests/test_three_way_merge.py
@@ -556,7 +556,7 @@ class TestLoadShazamState:
                 'origin': 'https://example.com',
                 'merge_flags': '--signoff',
             }
-            assert sf == state_file
+            assert os.path.normpath(sf) == os.path.normpath(state_file)
         finally:
             os.unlink(state_file)
             os.rmdir(patches_dir)
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 03/13] b4: add cross-platform username resolution in _setup_user_config
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 01/13] tests: specify UTF-8 encoding when opening files in text mode Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 02/13] tests: use os.path.join/normpath for portable path assertions Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 04/13] b4: resolve platform-native data and cache directories Adrian Neftali Sanchez
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

On Unix-like systems, obtain the user's display name from pwd.getpwuid()
when GIT_COMMITTER_NAME and GIT_AUTHOR_NAME are not set.  Parse pw_gecos
to extract only the full name field (before the first comma) per POSIX
conventions.

On Windows, where pwd and os.getuid() are unavailable, fall back to the
USERNAME environment variable.  Use explicit sys.platform != 'win32' guards
so static type checkers (mypy, pyright, ruff) correctly recognize platform-
specific APIs without 'possibly unbound' or 'attr-defined' diagnostics.

This enables b4 to initialize USER_CONFIG['name'] reliably across all
supported platforms without raising ImportError or AttributeError.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/__init__.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/b4/__init__.py b/src/b4/__init__.py
index 0530fec..e878d34 100644
--- a/src/b4/__init__.py
+++ b/src/b4/__init__.py
@@ -17,7 +17,6 @@ import logging
 import mailbox
 import os
 import pathlib
-import pwd
 import re
 import shlex
 import shutil
@@ -4007,9 +4006,15 @@ def _setup_user_config(cmdargs: argparse.Namespace) -> None:
             USER_CONFIG['name'] = os.environ['GIT_COMMITTER_NAME']
         elif 'GIT_AUTHOR_NAME' in os.environ:
             USER_CONFIG['name'] = os.environ['GIT_AUTHOR_NAME']
-        else:
+        elif sys.platform != 'win32':
+            # Unix-like: pwd and os.getuid() are guaranteed in this branch
+            import pwd
             udata = pwd.getpwuid(os.getuid())
-            USER_CONFIG['name'] = udata.pw_gecos.strip(',')
+            # pw_gecos is typically "Full Name,Office,Phone,...". Extract just the name.
+            USER_CONFIG['name'] = udata.pw_gecos.split(',', 1)[0].strip()
+        else:
+            # Windows fallback
+            USER_CONFIG['name'] = os.environ.get('USERNAME', 'unknown')
     if 'email' not in USER_CONFIG:
         if 'GIT_COMMITTER_EMAIL' in os.environ:
             USER_CONFIG['email'] = os.environ['GIT_COMMITTER_EMAIL']
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 04/13] b4: resolve platform-native data and cache directories
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (2 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 03/13] b4: add cross-platform username resolution in _setup_user_config Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 05/13] b4: normalise git-reported absolute paths with os.path.normpath Adrian Neftali Sanchez
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

XDG_DATA_HOME and XDG_CACHE_HOME are honoured when set, covering custom
Linux/macOS setups.  Add a Windows branch that reads APPDATA (for data)
and LOCALAPPDATA (for cache) before falling back to the
pathlib.Path.home() equivalents, matching the Windows platform
convention.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/__init__.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/b4/__init__.py b/src/b4/__init__.py
index e878d34..bddf2db 100644
--- a/src/b4/__init__.py
+++ b/src/b4/__init__.py
@@ -3896,6 +3896,8 @@ def get_main_config() -> ConfigDictT:
 def get_data_dir(appname: str = 'b4') -> str:
     if 'XDG_DATA_HOME' in os.environ:
         datahome = os.environ['XDG_DATA_HOME']
+    elif sys.platform == 'win32':
+        datahome = os.environ.get('APPDATA', str(pathlib.Path.home() / 'AppData' / 'Roaming'))
     else:
         datahome = os.path.join(str(pathlib.Path.home()), '.local', 'share')
     datadir = os.path.join(datahome, appname)
@@ -3907,6 +3909,8 @@ def get_cache_dir(appname: str = 'b4') -> str:
     global _CACHE_CLEANED
     if 'XDG_CACHE_HOME' in os.environ:
         cachehome = os.environ['XDG_CACHE_HOME']
+    elif sys.platform == 'win32':
+        cachehome = os.environ.get('LOCALAPPDATA', str(pathlib.Path.home() / 'AppData' / 'Local'))
     else:
         cachehome = os.path.join(str(pathlib.Path.home()), '.cache')
     cachedir = os.path.join(cachehome, appname)
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 05/13] b4: normalise git-reported absolute paths with os.path.normpath
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (3 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 04/13] b4: resolve platform-native data and cache directories Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 06/13] b4: select platform-appropriate default pager Adrian Neftali Sanchez
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

git rev-parse --git-common-dir emits forward-slash paths on Windows even
though the OS uses backslashes.  Pass the result through
os.path.normpath so all callers receive a path in the native format on
every platform.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/__init__.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/b4/__init__.py b/src/b4/__init__.py
index bddf2db..262668f 100644
--- a/src/b4/__init__.py
+++ b/src/b4/__init__.py
@@ -4534,6 +4534,11 @@ def git_get_common_dir(path: Optional[str] = None) -> Optional[str]:
             topdir = git_get_toplevel(path)
             if topdir:
                 result = os.path.normpath(os.path.join(topdir, result))
+        else:
+            # Normalize absolute paths to use the OS-native separator.
+            # On Windows, git outputs forward-slash paths; normpath converts
+            # them to backslashes so all callers see a consistent format.
+            result = os.path.normpath(result)
         return result
     return None
 
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 06/13] b4: select platform-appropriate default pager
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (4 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 05/13] b4: normalise git-reported absolute paths with os.path.normpath Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 07/13] ez: use portable uid/gid accessors in write_to_tar Adrian Neftali Sanchez
                   ` (6 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

"less" is not available on a stock Windows installation.  Fall back to
"more" when sys.platform is "win32" and no explicit pager is configured
via GIT_PAGER, core.pager, or the PAGER environment variable.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/b4/__init__.py b/src/b4/__init__.py
index 262668f..803d487 100644
--- a/src/b4/__init__.py
+++ b/src/b4/__init__.py
@@ -5685,7 +5685,7 @@ def view_in_pager(bdata: bytes, filehint: str = 'b4-view.txt') -> None:
         os.environ.get('GIT_PAGER')
         or corecfg.get('pager')
         or os.environ.get('PAGER')
-        or 'less'
+        or ('more' if sys.platform == 'win32' else 'less')
     )
     logger.debug('pager=%s', pager)
 
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 07/13] ez: use portable uid/gid accessors in write_to_tar
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (5 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 06/13] b4: select platform-appropriate default pager Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 08/13] tui: The _suspend_to_shell function assumes a POSIX environment, relying on $SHELL and shell-specific arguments (e.g., --rcfile) that are invalid on Windows Adrian Neftali Sanchez
                   ` (5 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

os.getuid and os.getgid are POSIX-only; calling them directly raises
AttributeError on Windows.  Use getattr with a lambda returning 0 so tar
entries are created with a harmless numeric uid/gid on platforms that do
not have POSIX user identifiers.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/ez.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/b4/ez.py b/src/b4/ez.py
index 3e81138..51fbdc9 100644
--- a/src/b4/ez.py
+++ b/src/b4/ez.py
@@ -2800,8 +2800,8 @@ def write_to_tar(
 ) -> None:
     tifo = tarfile.TarInfo(name)
     tuser = os.environ.get('USERNAME', 'user')
-    tuid = os.getuid()
-    tgid = os.getgid()
+    tuid = getattr(os, 'getuid', lambda: 0)()
+    tgid = getattr(os, 'getgid', lambda: 0)()
     tifo.uid = tuid
     tifo.gid = tgid
     tifo.uname = tuser
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 08/13] tui: The _suspend_to_shell function assumes a POSIX environment, relying on $SHELL and shell-specific arguments (e.g., --rcfile) that are invalid on Windows.
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (6 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 07/13] ez: use portable uid/gid accessors in write_to_tar Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 09/13] tests: replace NamedTemporaryFile with tmp_path in patatt fixture Adrian Neftali Sanchez
                   ` (4 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Refactor the execution flow to branch on sys.platform. On Windows,
launch the native interpreter via COMSPEC (defaulting to cmd.exe).
Move the existing POSIX-specific logic for bash/zsh into an explicit
else-block to ensure clean separation of platform execution paths.

This enables Windows support while preventing POSIX shell configuration
side effects on non-POSIX systems.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/tui/_common.py | 58 ++++++++++++++++++++++++-------------------
 1 file changed, 32 insertions(+), 26 deletions(-)

diff --git a/src/b4/tui/_common.py b/src/b4/tui/_common.py
index cdd3818..6c8042c 100644
--- a/src/b4/tui/_common.py
+++ b/src/b4/tui/_common.py
@@ -10,6 +10,7 @@ __author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>'
 import email.utils
 import os
 import subprocess
+import sys
 import tempfile
 import unicodedata
 from collections import defaultdict
@@ -198,36 +199,41 @@ def _suspend_to_shell(hint: str = 'b4', cwd: Optional[str] = None) -> None:
     logger.info('When done, Ctrl-d to return to review UI.')
     logger.info('---')
 
-    shell = os.environ.get('SHELL', '/bin/sh')
-    shellname = os.path.basename(shell)
     env = os.environ.copy()
     env['B4_REVIEW'] = hint
 
-    if shellname == 'bash':
-        bashrc = os.path.expanduser('~/.bashrc')
-        source = f'[ -f {bashrc} ] && . {bashrc}\n'
-        source += f'PS1="({hint}) $PS1"\n'
-        with tempfile.NamedTemporaryFile(
-            mode='w', prefix='b4-shell-', suffix='.sh', delete=False
-        ) as rcf:
-            rcf.write(source)
-            rcfile = rcf.name
-        try:
-            subprocess.run([shell, '--rcfile', rcfile], env=env, cwd=cwd)
-        finally:
-            os.unlink(rcfile)
-    elif shellname == 'zsh':
-        real_zdotdir = os.environ.get('ZDOTDIR', os.path.expanduser('~'))
-        with tempfile.TemporaryDirectory(prefix='b4-shell-') as tmpdir:
-            zshrc = os.path.join(tmpdir, '.zshrc')
-            with open(zshrc, 'w') as f:
-                f.write(f'ZDOTDIR="{real_zdotdir}"\n')
-                f.write('[ -f "$ZDOTDIR/.zshrc" ] && . "$ZDOTDIR/.zshrc"\n')
-                f.write(f'PS1="({hint}) $PS1"\n')
-            env['ZDOTDIR'] = tmpdir
-            subprocess.run([shell], env=env, cwd=cwd)
-    else:
+    if sys.platform == 'win32':
+        shell = os.environ.get('COMSPEC', 'cmd.exe')
         subprocess.run([shell], env=env, cwd=cwd)
+    else:
+        shell = os.environ.get('SHELL', '/bin/sh')
+        shellname = os.path.basename(shell)
+
+        if shellname == 'bash':
+            bashrc = os.path.expanduser('~/.bashrc')
+            source = f'[ -f {bashrc} ] && . {bashrc}\n'
+            source += f'PS1="({hint}) $PS1"\n'
+            with tempfile.NamedTemporaryFile(
+                mode='w', prefix='b4-shell-', suffix='.sh', delete=False
+            ) as rcf:
+                rcf.write(source)
+                rcfile = rcf.name
+            try:
+                subprocess.run([shell, '--rcfile', rcfile], env=env, cwd=cwd)
+            finally:
+                os.unlink(rcfile)
+        elif shellname == 'zsh':
+            real_zdotdir = os.environ.get('ZDOTDIR', os.path.expanduser('~'))
+            with tempfile.TemporaryDirectory(prefix='b4-shell-') as tmpdir:
+                zshrc = os.path.join(tmpdir, '.zshrc')
+                with open(zshrc, 'w') as f:
+                    f.write(f'ZDOTDIR="{real_zdotdir}"\n')
+                    f.write('[ -f "$ZDOTDIR/.zshrc" ] && . "$ZDOTDIR/.zshrc"\n')
+                    f.write(f'PS1="({hint}) $PS1"\n')
+                env['ZDOTDIR'] = tmpdir
+                subprocess.run([shell], env=env, cwd=cwd)
+        else:
+            subprocess.run([shell], env=env, cwd=cwd)
 
 
 def _addrs_to_lines(header_str: str) -> str:
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 09/13] tests: replace NamedTemporaryFile with tmp_path in patatt fixture
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (7 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 08/13] tui: The _suspend_to_shell function assumes a POSIX environment, relying on $SHELL and shell-specific arguments (e.g., --rcfile) that are invalid on Windows Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 10/13] pyproject: add Windows operating-system classifier Adrian Neftali Sanchez
                   ` (3 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Use pytest's built-in tmp_path fixture to store the ephemeral ed25519
private key instead of tempfile.NamedTemporaryFile.  This removes the
manual os.unlink teardown step (pytest removes the directory
automatically, even when a test raises) and also avoids the Windows
restriction that a NamedTemporaryFile with delete=True cannot be opened
by a second process while still open.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/tests/test_patatt.py | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/src/tests/test_patatt.py b/src/tests/test_patatt.py
index de05277..3ed4711 100644
--- a/src/tests/test_patatt.py
+++ b/src/tests/test_patatt.py
@@ -6,6 +6,7 @@ Uses ephemeral ed25519 keys so no external key material is needed.
 import base64
 import email.message
 import os
+import pathlib
 import tempfile
 from collections.abc import Generator
 from typing import Tuple, Union
@@ -18,21 +19,29 @@ import patatt
 
 
 @pytest.fixture()
-def ed25519_keypair() -> Generator[Tuple[str, str, str, str], None, None]:
-    """Generate an ephemeral ed25519 keypair written to temp files.
-
-    Returns (privkey_path, verify_key_b64, identity, selector).
-    The private key file is written so patatt can find it via
-    signingkey = ed25519:/path/to/key.
+def ed25519_keypair(
+    tmp_path: pathlib.Path,
+    monkeypatch: pytest.MonkeyPatch,
+) -> Generator[Tuple[str, str, str, str], None, None]:
+    """Generate an ephemeral ed25519 keypair stored in patatt's data directory.
+
+    Returns (key_name, verify_key_b64, identity, selector).  The private key
+    is written to ``<tmp_path>/patatt/private/<key_name>.key``.  XDG_DATA_HOME
+    is pointed at ``tmp_path`` for the duration of the test so patatt resolves
+    the key by name without any OS-specific absolute-path handling.
+    patatt.KEYCACHE is cleared to prevent key material from a previous test
+    leaking into this one (all tests share the identity "test@example.com").
     """
     sk = SigningKey.generate()
     sk_b64 = base64.b64encode(sk.encode()).decode()
     vk_b64 = base64.b64encode(sk.verify_key.encode()).decode()
-    with tempfile.NamedTemporaryFile(mode='w', suffix='.key', delete=False) as fh:
-        fh.write(sk_b64)
-        privkey_path = fh.name
-    yield privkey_path, vk_b64, 'test@example.com', 'default'
-    os.unlink(privkey_path)
+    key_name = 'test-key'
+    private_dir = tmp_path / 'patatt' / 'private'
+    private_dir.mkdir(parents=True, exist_ok=True)
+    (private_dir / f'{key_name}.key').write_text(sk_b64)
+    monkeypatch.setenv('XDG_DATA_HOME', str(tmp_path))
+    monkeypatch.setattr(patatt, 'KEYCACHE', {})
+    yield key_name, vk_b64, 'test@example.com', 'default'
 
 
 @pytest.fixture()
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 10/13] pyproject: add Windows operating-system classifier
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (8 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 09/13] tests: replace NamedTemporaryFile with tmp_path in patatt fixture Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 11/13] ci: add ci.ps1 for running the quality-gate pipeline on Windows Adrian Neftali Sanchez
                   ` (2 subsequent siblings)
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Declare that b4 supports Windows by adding the
"Operating System :: Microsoft :: Windows" trove classifier.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 pyproject.toml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pyproject.toml b/pyproject.toml
index 96e2fa5..2522930 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,6 +16,7 @@ requires-python = ">=3.11"
 classifiers = [
     "Environment :: Console",
     "Operating System :: POSIX :: Linux",
+    "Operating System :: Microsoft :: Windows",
     "Intended Audience :: Developers",
     "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",
     "Topic :: Communications :: Email",
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 11/13] ci: add ci.ps1 for running the quality-gate pipeline on Windows
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (9 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 10/13] pyproject: add Windows operating-system classifier Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 12/13] review_tui: write git tracking commit before updating DB Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 13/13] review_tui: guard against unmounted diff-viewer In certain asynchronous contexts or during rapid UI transitions, _show_content may be invoked before the diff-viewer widget has been fully mounted in the DOM. This is particularly reproducible in headless test environments and on platforms with slower console I/O initialization Adrian Neftali Sanchez
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Provide a PowerShell equivalent of ci.sh that runs the full quality-gate
pipeline (dependency sync, format check, linting, type checking, tests)
via uv.  This makes it straightforward to validate the Windows port
locally or in a Windows CI environment without requiring a POSIX shell.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 ci.ps1 | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 ci.ps1

diff --git a/ci.ps1 b/ci.ps1
new file mode 100644
index 0000000..1eaceb7
--- /dev/null
+++ b/ci.ps1
@@ -0,0 +1,15 @@
+#!/usr/bin/env pwsh
+# Windows equivalent of ci.sh — runs the full quality-gate pipeline via uv.
+# Prerequisites: uv (https://docs.astral.sh/uv/), Python 3.11+, Git for Windows.
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+uv sync --all-extras --all-groups
+
+uv run ruff format --check
+uv run ruff check
+uv run ty check
+uv run mypy .
+uv run pyright
+uv run pytest --durations=20
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 12/13] review_tui: write git tracking commit before updating DB
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (10 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 11/13] ci: add ci.ps1 for running the quality-gate pipeline on Windows Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  2026-04-25 19:49 ` [RFC PATCH 13/13] review_tui: guard against unmounted diff-viewer In certain asynchronous contexts or during rapid UI transitions, _show_content may be invoked before the diff-viewer widget has been fully mounted in the DOM. This is particularly reproducible in headless test environments and on platforms with slower console I/O initialization Adrian Neftali Sanchez
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

The background rescan_branches worker reads the git tracking commit and
writes the status it finds back to the DB.  Both action_review and
action_waiting previously wrote to the DB first and updated the git
tracking commit second; if the rescan worker woke up in that window it
would overwrite the DB row with the stale status from the old commit.

On Linux this window is rarely hit because epoll wakes the event loop
only when I/O is ready.  Windows uses IocpProactor, which processes
completion callbacks eagerly and causes the background rescan task to
be scheduled between the two writes reliably, making the race
consistently reproducible in the test suite.

Fix both actions so the git tracking commit is always written first.
The rescan then reads the already-updated commit and any DB write it
performs is idempotent with what the action itself is about to write.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/review_tui/_tracking_app.py | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/src/b4/review_tui/_tracking_app.py b/src/b4/review_tui/_tracking_app.py
index a5a9389..d52a49b 100644
--- a/src/b4/review_tui/_tracking_app.py
+++ b/src/b4/review_tui/_tracking_app.py
@@ -1245,16 +1245,20 @@ class TrackingApp(CheckRunnerMixin, App[Optional[str]]):
                             conn, change_id, 'reviewing', revision=revision
                         )
                 elif status in ('waiting', 'accepted'):
-                    # Bring back to reviewing on re-entry
-                    if conn:
-                        b4.review.tracking.update_series_status(
-                            conn, change_id, 'reviewing', revision=revision
-                        )
+                    # Bring back to reviewing on re-entry.
+                    # Update git FIRST so that any concurrent rescan reads the
+                    # new status from the tracking commit before we commit it
+                    # to the DB; this prevents the rescan from overwriting the
+                    # DB with the stale status on the next wake-up.
                     topdir = b4.git_get_toplevel()
                     if topdir:
                         b4.review.update_tracking_status(
                             topdir, branch_name, 'reviewing'
                         )
+                    if conn:
+                        b4.review.tracking.update_series_status(
+                            conn, change_id, 'reviewing', revision=revision
+                        )
                 # Clear the followup badge — user is about to read this series
                 if conn and self._identifier and isinstance(revision, int):
                     b4.review.tracking.mark_all_messages_seen(conn, change_id, revision)
@@ -4078,6 +4082,12 @@ class TrackingApp(CheckRunnerMixin, App[Optional[str]]):
             return
         change_id = self._selected_series.get('change_id', '')
         revision = self._selected_series.get('revision')
+        # Update git FIRST so that any concurrent rescan reads the new status
+        # from the tracking commit before we commit it to the DB.
+        topdir = b4.git_get_toplevel()
+        if topdir and status != 'new':
+            branch_name = f'b4/review/{change_id}'
+            b4.review.update_tracking_status(topdir, branch_name, 'waiting')
         try:
             conn = b4.review.tracking.get_db(self._identifier)
             b4.review.tracking.update_series_status(
@@ -4087,10 +4097,6 @@ class TrackingApp(CheckRunnerMixin, App[Optional[str]]):
         except Exception as ex:
             self.notify(f'Error: {ex}', severity='error')
             return
-        topdir = b4.git_get_toplevel()
-        if topdir and status != 'new':
-            branch_name = f'b4/review/{change_id}'
-            b4.review.update_tracking_status(topdir, branch_name, 'waiting')
         self.notify('Series moved to waiting')
         self._focus_change_id = change_id
         self._invalidate_caches(change_id)
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [RFC PATCH 13/13] review_tui: guard against unmounted diff-viewer In certain asynchronous contexts or during rapid UI transitions, _show_content may be invoked before the diff-viewer widget has been fully mounted in the DOM. This is particularly reproducible in headless test environments and on platforms with slower console I/O initialization.
  2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
                   ` (11 preceding siblings ...)
  2026-04-25 19:49 ` [RFC PATCH 12/13] review_tui: write git tracking commit before updating DB Adrian Neftali Sanchez
@ 2026-04-25 19:49 ` Adrian Neftali Sanchez
  12 siblings, 0 replies; 14+ messages in thread
From: Adrian Neftali Sanchez @ 2026-04-25 19:49 UTC (permalink / raw)
  To: tools; +Cc: konstantin, Adrian Neftali Sanchez

Add a defensive try-except block to catch NoMatches when querying for
'#diff-viewer'. If the widget is not yet available, log a debug message
and return early to prevent an application crash.

Signed-off-by: Adrian Neftali Sanchez <lutgaru@gmail.com>
---
 src/b4/review_tui/_review_app.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/b4/review_tui/_review_app.py b/src/b4/review_tui/_review_app.py
index 8786fbb..e6d6e4e 100644
--- a/src/b4/review_tui/_review_app.py
+++ b/src/b4/review_tui/_review_app.py
@@ -18,6 +18,7 @@ from rich.text import Text
 from textual.app import App, ComposeResult
 from textual.binding import Binding
 from textual.containers import Horizontal, Vertical
+from textual.css.query import NoMatches
 from textual.events import Click
 from textual.widgets import Label, ListItem, ListView, RichLog, Static
 
@@ -458,7 +459,12 @@ class ReviewApp(CheckRunnerMixin, App[None]):
         total = len(self._commit_shas)
         if display_idx < 0 or display_idx > total:
             return
-        viewer = self.query_one('#diff-viewer', RichLog)
+        # Defensive: widget may not be mounted yet in headless/async tests
+        try:
+            viewer = self.query_one('#diff-viewer', RichLog)
+        except NoMatches:
+            self.log.debug("diff-viewer not ready for patch %s", display_idx)
+            return
         viewer.clear()
         self._comment_positions = []
         self._followup_positions = {}
-- 
2.45.0.windows.1


^ permalink raw reply related	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-04-25 19:50 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-25 19:49 [RFC PATCH 00/13] b4: add native Windows support Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 01/13] tests: specify UTF-8 encoding when opening files in text mode Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 02/13] tests: use os.path.join/normpath for portable path assertions Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 03/13] b4: add cross-platform username resolution in _setup_user_config Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 04/13] b4: resolve platform-native data and cache directories Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 05/13] b4: normalise git-reported absolute paths with os.path.normpath Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 06/13] b4: select platform-appropriate default pager Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 07/13] ez: use portable uid/gid accessors in write_to_tar Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 08/13] tui: The _suspend_to_shell function assumes a POSIX environment, relying on $SHELL and shell-specific arguments (e.g., --rcfile) that are invalid on Windows Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 09/13] tests: replace NamedTemporaryFile with tmp_path in patatt fixture Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 10/13] pyproject: add Windows operating-system classifier Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 11/13] ci: add ci.ps1 for running the quality-gate pipeline on Windows Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 12/13] review_tui: write git tracking commit before updating DB Adrian Neftali Sanchez
2026-04-25 19:49 ` [RFC PATCH 13/13] review_tui: guard against unmounted diff-viewer In certain asynchronous contexts or during rapid UI transitions, _show_content may be invoked before the diff-viewer widget has been fully mounted in the DOM. This is particularly reproducible in headless test environments and on platforms with slower console I/O initialization Adrian Neftali Sanchez

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.