* [PATCH b4 v2] ez: allow cleaning multiple branches at once
@ 2026-02-03 9:54 Antonin Godard
2026-02-24 19:04 ` Konstantin Ryabitsev
0 siblings, 1 reply; 2+ messages in thread
From: Antonin Godard @ 2026-02-03 9:54 UTC (permalink / raw)
To: Kernel.org Tools; +Cc: Konstantin Ryabitsev, Thomas Petazzoni, Antonin Godard
Allow "b4 prep --cleanup" to cleanup multiple branches in one go. If an
error occurs (branch not known/empty branch/currently checked out), just
return to continue cleaning up branches. Give the user multiple options:
"y" to cleanup, "n" to skip, "q" to abort the cleanup, "s" to show
the branch log with diffs, and "?" to print help about these options.
Passing nothing to --cleanup still shows the available branches to
cleanup.
Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
---
Changes in v2:
- return instead of failing if branch is not prep-managed
- show branch log instead of general information on the branch
- Link to v1: https://patch.msgid.link/20251211-multiple-prep-cleanup-v1-1-c180f87248c4@bootlin.com
---
docs/contributor/prep.rst | 10 ++---
src/b4/command.py | 4 +-
src/b4/ez.py | 94 ++++++++++++++++++++++++++++++++---------------
3 files changed, 72 insertions(+), 36 deletions(-)
diff --git a/docs/contributor/prep.rst b/docs/contributor/prep.rst
index 18c710d..6d6fed5 100644
--- a/docs/contributor/prep.rst
+++ b/docs/contributor/prep.rst
@@ -337,15 +337,15 @@ up the prep-managed branch, together with all of its sent tags::
b4 prep --cleanup
-This command lists all prep-managed branches in your repository. Pick a
-branch to clean up, make sure it's not currently checked out, and run
-the command again::
+This command lists all prep-managed branches in your repository. Pick one or
+more branches to clean up, make sure it's not currently checked out, and run the
+command again::
- b4 prep --cleanup b4/my-topical-branch
+ b4 prep --cleanup b4/my-topical-branch ...
After you confirm your action, this should create a tarball with all the
patches, cover letters, and tracking information from your series.
-Afterwards, b4 deletes the branch and all related tags from your local
+Afterwards, b4 deletes the branch(es) and all related tags from your local
repository.
.. _prep_flags:
diff --git a/src/b4/command.py b/src/b4/command.py
index 1f8b8f1..cc8bc38 100644
--- a/src/b4/command.py
+++ b/src/b4/command.py
@@ -344,8 +344,8 @@ def setup_parser() -> argparse.ArgumentParser:
help='Mark current revision as sent and reroll (requires cover letter msgid)')
spp_g.add_argument('--show-info', metavar='PARAM', nargs='?', const=':_all',
help='Show series info in a format that can be passed to other commands.')
- spp_g.add_argument('--cleanup', metavar='BRANCHNAME', nargs='?', const='_show',
- help='Archive and remove a prep-tracked branch and all its sent/ tags')
+ spp_g.add_argument('--cleanup', metavar='BRANCHNAME', nargs='*',
+ help='Archive and remove prep-tracked branches and all associated sent/ tags')
ag_prepn = sp_prep.add_argument_group('Create new branch', 'Create a new branch for working on patch series')
ag_prepn.add_argument('-n', '--new', dest='new_series_name',
diff --git a/src/b4/ez.py b/src/b4/ez.py
index 298e382..7af8e14 100644
--- a/src/b4/ez.py
+++ b/src/b4/ez.py
@@ -26,6 +26,7 @@ import io
import tarfile
import hashlib
import urllib.parse
+import subprocess
from typing import Any, Optional, Tuple, List, Union, Dict, Set
from string import Template
@@ -2518,47 +2519,40 @@ def get_prep_managed_branches(gitdir: Optional[str] = None) -> List[str]:
return mybranches
-def cleanup(param: str) -> None:
- if param == '_show':
- # Show all b4-tracked branches
- mybranches = get_prep_managed_branches(None)
- if not len(mybranches):
- logger.info('No b4-tracked branches found')
- sys.exit(0)
+def _cleanup_branch(branch: str) -> None:
- logger.info('Please specify branch:')
- for branch in mybranches:
- logger.info(' %s', branch)
+ if not b4.git_branch_exists(None, branch):
+ logger.error('ERROR: Not a known branch: %s', branch)
return
- mybranch = param
- if not b4.git_branch_exists(None, mybranch):
- logger.critical('Not a known branch: %s', mybranch)
- sys.exit(1)
- is_prep_branch(mustbe=True, usebranch=mybranch)
- base_commit, start_commit, end_commit = get_series_range(usebranch=mybranch)
+ if not is_prep_branch(usebranch=branch):
+ return
+
+ base_commit, start_commit, end_commit = get_series_range(usebranch=branch)
# start commit and end commit can't be the same
if start_commit == end_commit:
- logger.critical('CRITICAL: %s appears to be an empty branch', mybranch)
- sys.exit(1)
+ logger.error('ERROR: %s appears to be an empty branch', branch)
+ return
+
# Refuse to clean up the currently checked out branch
curbranch = b4.git_get_current_branch()
- if curbranch == mybranch:
- logger.critical('CRITICAL: %s is currently checked out, cannot cleanup', mybranch)
- sys.exit(1)
- cover, tracking = load_cover(usebranch=mybranch)
+ if curbranch == branch:
+ logger.error('ERROR: %s is currently checked out, cannot cleanup', branch)
+ return
+
+ cover, tracking = load_cover(usebranch=branch)
# Find all tags
ts = tracking['series']
tags = list()
- logger.info('Will archive and delete all of the following:')
+ logger.info('\nWill archive and delete all of the following:')
logger.info('---')
- logger.info('branch: %s', mybranch)
+ logger.info('branch: %s', branch)
if 'history' in ts:
for rn, links in ts['history'].items():
tagname, revision = get_sent_tagname(ts.get('change-id'), SENT_TAG_PREFIX, rn)
tag_commit = b4.git_revparse_tag(None, tagname)
if not tag_commit:
- tagname, revision = get_sent_tagname(mybranch, SENT_TAG_PREFIX, rn)
+ tagname, revision = get_sent_tagname(branch, SENT_TAG_PREFIX, rn)
tag_commit = b4.git_revparse_tag(None, tagname)
if not tag_commit:
logger.debug('No tag matching revision %s', revision)
@@ -2573,7 +2567,32 @@ def cleanup(param: str) -> None:
tags.append((tagname, base_commit, tag_commit, revision, cover))
logger.info('---')
try:
- input('Press Enter to confirm or Ctrl-C to abort')
+ resp = None
+ while resp is None:
+ resp = input('Proceed? [y/s/q/N/?] ')
+ if resp == "?":
+ logger.info(textwrap.dedent(
+ """
+ Possible answers:
+ y: cleanup the branch
+ s: show branch log
+ q or Ctrl-C: abort cleanup
+ n (default): do not cleanup this branch
+ ?: show this help message
+ """))
+ resp = None
+ elif resp in ("show", "s"):
+ start_commit = get_info(branch)["start-commit"]
+ end_commit = get_info(branch)["end-commit"]
+ subprocess.run(["git", "--paginate", "log", "-p",
+ f"{start_commit}~..{end_commit}"])
+ logger.info('')
+ resp = None
+ elif resp == "q":
+ sys.exit(130)
+ elif resp != "y":
+ return
+
except KeyboardInterrupt:
logger.info('')
sys.exit(130)
@@ -2595,13 +2614,13 @@ def cleanup(param: str) -> None:
write_to_tar(tfh, f'{change_id}/tracking.js', mnow, ifh)
ifh.close()
# Add the current series
- logger.info('Archiving branch %s', mybranch)
+ logger.info('Archiving branch %s', branch)
patches = b4.git_range_to_patches(None, start_commit, end_commit)
ifh = io.BytesIO()
b4.save_git_am_mbox([patch[1] for patch in patches], ifh)
write_to_tar(tfh, f'{change_id}/patches.mbx', mnow, ifh)
ifh.close()
- deletes.append(['branch', '--delete', '--force', mybranch])
+ deletes.append(['branch', '--delete', '--force', branch])
for tagname, base_commit, tag_commit, revision, cover in tags:
logger.info('Archiving %s', tagname)
@@ -2636,6 +2655,23 @@ def cleanup(param: str) -> None:
logger.info('Wrote: %s', tarpath)
+def cleanup(branches: list[str]) -> None:
+ if not branches:
+ # Show all b4-tracked branches
+ mybranches = get_prep_managed_branches(None)
+ if not len(mybranches):
+ logger.info('No b4-tracked branches found')
+ sys.exit(0)
+
+ logger.info('Please specify branch:')
+ for branch in mybranches:
+ logger.info(' %s', branch)
+ return
+
+ for branch in branches:
+ _cleanup_branch(branch)
+
+
def show_info(param: str) -> None:
# is param a name of the branch?
mybranch: Optional[str] = None
@@ -3027,7 +3063,7 @@ def cmd_prep(cmdargs: argparse.Namespace) -> None:
if cmdargs.show_info:
return show_info(cmdargs.show_info)
- if cmdargs.cleanup:
+ if cmdargs.cleanup is not None:
return cleanup(cmdargs.cleanup)
if cmdargs.format_patch:
---
base-commit: 477734000555ffc24bf873952e40367deee26f17
change-id: 20250407-multiple-prep-cleanup-b60966ba8bf5
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH b4 v2] ez: allow cleaning multiple branches at once
2026-02-03 9:54 [PATCH b4 v2] ez: allow cleaning multiple branches at once Antonin Godard
@ 2026-02-24 19:04 ` Konstantin Ryabitsev
0 siblings, 0 replies; 2+ messages in thread
From: Konstantin Ryabitsev @ 2026-02-24 19:04 UTC (permalink / raw)
To: Antonin Godard; +Cc: Kernel.org Tools, Konstantin Ryabitsev, Thomas Petazzoni
On Tue, 03 Feb 2026 10:54:01 +0100, Antonin Godard <antonin.godard@bootlin.com> wrote:
> Allow "b4 prep --cleanup" to cleanup multiple branches in one go. If an
> error occurs (branch not known/empty branch/currently checked out), just
> return to continue cleaning up branches. Give the user multiple options:
> "y" to cleanup, "n" to skip, "q" to abort the cleanup, "s" to show
> the branch log with diffs, and "?" to print help about these options.
>
> Passing nothing to --cleanup still shows the available branches to
> cleanup.
Thanks for sending this in! I'm not against the feature, but there are
several places where I think this can be improved.
> diff --git a/src/b4/ez.py b/src/b4/ez.py
> index 298e382..7af8e14 100644
> --- a/src/b4/ez.py
> +++ b/src/b4/ez.py
> @@ -26,6 +26,7 @@ import io
> import tarfile
> import hashlib
> import urllib.parse
> +import subprocess
This import is only needed for the single subprocess.run() call in the
"s" handler. Consider using b4.git_run_command() instead, which would
eliminate the need for this import entirely.
> @@ -2518,47 +2519,40 @@ def get_prep_managed_branches(gitdir: Optional[str] = None) -> List[str]:
> return mybranches
>
>
> -def cleanup(param: str) -> None:
> - if param == '_show':
> - # Show all b4-tracked branches
> - mybranches = get_prep_managed_branches(None)
> - if not len(mybranches):
> - logger.info('No b4-tracked branches found')
> - sys.exit(0)
> +def _cleanup_branch(branch: str) -> None:
>
> - logger.info('Please specify branch:')
> - for branch in mybranches:
> - logger.info(' %s', branch)
> + if not b4.git_branch_exists(None, branch):
> + logger.error('ERROR: Not a known branch: %s', branch)
> return
>
> - mybranch = param
> - if not b4.git_branch_exists(None, mybranch):
> - logger.critical('Not a known branch: %s', mybranch)
> - sys.exit(1)
> - is_prep_branch(mustbe=True, usebranch=mybranch)
> - base_commit, start_commit, end_commit = get_series_range(usebranch=mybranch)
> + if not is_prep_branch(usebranch=branch):
is_prep_branch() without mustbe=True returns False silently. The user
gets no indication of why this branch was skipped. Consider adding an
error message here, e.g.:
if not is_prep_branch(usebranch=branch):
logger.error('ERROR: %s is not a prep-managed branch', branch)
return
> @@ -2573,7 +2567,32 @@ def cleanup(param: str) -> None:
> tags.append((tagname, base_commit, tag_commit, revision, cover))
> logger.info('---')
> try:
> - input('Press Enter to confirm or Ctrl-C to abort')
> + resp = None
> + while resp is None:
> + resp = input('Proceed? [y/s/q/N/?] ')
> + if resp == "?":
> + logger.info(textwrap.dedent(
> + """
> + Possible answers:
> + y: cleanup the branch
> + s: show branch log
> + q or Ctrl-C: abort cleanup
> + n (default): do not cleanup this branch
> + ?: show this help message
> + """))
> + resp = None
> + elif resp in ("show", "s"):
> + start_commit = get_info(branch)["start-commit"]
> + end_commit = get_info(branch)["end-commit"]
start_commit and end_commit are already computed at the top of this
function via get_series_range(). Re-fetching them from get_info() is
wasteful (get_info computes recipients, prereqs, preflight checks, etc.)
and also shadows the outer local variables. If the user presses "s" then
"y", the archiving code below will use these reassigned values.
Just use the already-available locals directly, e.g.:
b4.git_run_command(None, ['log', '-p',
f'{start_commit}~..{end_commit}'])
> + subprocess.run(["git", "--paginate", "log", "-p",
> + f"{start_commit}~..{end_commit}"])
Same as above, this should be using git_run_command().
> @@ -2636,6 +2655,23 @@ def cleanup(param: str) -> None:
> logger.info('Wrote: %s', tarpath)
>
>
> +def cleanup(branches: list[str]) -> None:
Small nitpick: the lowercase list[str] type annotation requires Python 3.9+.
The rest of this file uses List from typing (already imported). Use List[str]
for consistency.
Cheers,
--
KR
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-02-24 19:05 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-03 9:54 [PATCH b4 v2] ez: allow cleaning multiple branches at once Antonin Godard
2026-02-24 19:04 ` Konstantin Ryabitsev
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox