From: Jasper Mayone Date: Wed, 21 Oct 2026 17:33:37 GMT Subject: [RFC PATCH 1/1] worktree list: add --merged and --no-merged Message-ID: <20261021173350.67854-2-me@jaspermayone.com> In-Reply-To: <20261021173350.67854-1-me@jaspermayone.com> Users who keep one worktree per topic have no direct way to find the worktrees whose work has already landed. "git branch --merged" lists the branches, but the user then has to match each branch to a worktree by hand, and a detached worktree has no branch to match. "git branch --delete-merged" skips any branch that is checked out in a worktree, so these are exactly the branches it leaves behind. Teach "git worktree list" the same --merged and --no-merged filters that "git branch", "git tag" and "git for-each-ref" have, applied to the HEAD of each worktree. The options are parsed by the same OPT_MERGED and OPT_NO_MERGED helpers, so the default (the HEAD of the current worktree), the error messages and the rules for combining them are the same: a worktree is shown when its HEAD is reachable from at least one of the --merged commits and from none of the --no-merged commits. A bare worktree, a worktree on an unborn branch, and a worktree whose HEAD cannot be read have no HEAD commit, so the filters leave them out. Both output formats are kept, so the result can be used directly with "--porcelain -z": git worktree list --porcelain -z --merged origin/main Nothing is removed. Removing the worktrees stays a separate step with "git worktree remove", which already refuses a dirty or locked worktree. Signed-off-by: Jasper Mayone --- Documentation/git-worktree.adoc | 24 +++++- builtin/worktree.c | 80 +++++++++++++++++++- t/t2402-worktree-list.sh | 125 ++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc index e6e77252ab..817766b07c 100644 --- a/Documentation/git-worktree.adoc +++ b/Documentation/git-worktree.adoc @@ -11,7 +11,7 @@ SYNOPSIS [synopsis] git worktree add [-f] [--detach] [--checkout] [--lock [--reason ]] [--orphan] [(-b | -B) ] [] -git worktree list [-v | --porcelain [-z]] +git worktree list [-v | --porcelain [-z]] [--merged []] [--no-merged []] git worktree lock [--reason ] git worktree move git worktree prune [-n] [-v] [--expire ] @@ -114,6 +114,11 @@ whether the worktree is bare, the revision currently checked out, the branch currently checked out (or "detached HEAD" if none), "locked" if the worktree is locked, "prunable" if the worktree can be pruned by the `prune` command. ++ +With `--merged` or `--no-merged`, only the worktrees whose `HEAD` is +(or is not) reachable from the given commit are listed. This finds the +worktrees whose work has landed, for example to remove them with +`git worktree remove`. `lock`:: @@ -268,6 +273,23 @@ mismatch, even if the links are correct. to parse the output when a worktree path contains a newline character. +`--merged []`:: + With `list`, only list worktrees whose `HEAD` is reachable from + __. __ defaults to the `HEAD` of the current + worktree. + +`--no-merged []`:: + With `list`, only list worktrees whose `HEAD` is not reachable + from __. __ defaults to the `HEAD` of the current + worktree. ++ +When `--merged` and `--no-merged` are combined or repeated, only +worktrees whose `HEAD` is reachable from at least one of the +`--merged` commits and from none of the `--no-merged` commits are +listed. With either option, a bare worktree, a worktree on an unborn +branch, and a worktree whose `HEAD` cannot be read are not listed, +because they have no `HEAD` commit. + `-q`:: `--quiet`:: With `add`, suppress feedback messages. diff --git a/builtin/worktree.c b/builtin/worktree.c index 77ecd0f71f..5db9efb435 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -5,6 +5,7 @@ #include "abspath.h" #include "advice.h" #include "checkout.h" +#include "commit-reach.h" #include "config.h" #include "copy.h" #include "dir.h" @@ -18,8 +19,10 @@ #include "strvec.h" #include "branch.h" #include "read-cache-ll.h" +#include "ref-filter.h" #include "refs.h" #include "remote.h" +#include "revision.h" #include "run-command.h" #include "hook.h" #include "sigchain.h" @@ -33,7 +36,7 @@ " [--orphan] [(-b | -B) ] []") #define BUILTIN_WORKTREE_LIST_USAGE \ - N_("git worktree list [-v | --porcelain [-z]]") + N_("git worktree list [-v | --porcelain [-z]] [--merged []] [--no-merged []]") #define BUILTIN_WORKTREE_LOCK_USAGE \ N_("git worktree lock [--reason ] ") #define BUILTIN_WORKTREE_MOVE_USAGE \ @@ -1125,11 +1128,78 @@ static void pathsort(struct worktree **wt) QSORT(wt, n, pathcmp); } +/* + * Set result[i] when tips[i] is reachable from one of the commits in + * "bases". + */ +static void mark_reachable(struct repository *repo, + struct commit **tips, size_t nr, + struct commit_list *bases, int *result) +{ + tips_reachable_from_bases(repo, bases, tips, nr, UNINTERESTING); + for (size_t i = 0; i < nr; i++) + result[i] = !!(tips[i]->object.flags & UNINTERESTING); + clear_commit_marks_many(nr, tips, ALL_REV_FLAGS); + for (; bases; bases = bases->next) + clear_commit_marks(bases->item, ALL_REV_FLAGS); +} + +/* + * Keep only the worktrees whose HEAD is reachable from one of the + * "--merged" commits and from none of the "--no-merged" commits, like + * the ref filters of "git branch". A bare or unborn worktree has no + * HEAD commit, and neither has a broken one, so these are dropped. + */ +static void filter_merged(struct repository *repo, struct worktree **wt, + struct ref_filter *filter) +{ + struct commit **tips = NULL; + int *merged, *unmerged; + size_t nr = 0, alloc = 0, kept = 0; + + for (size_t i = 0; wt[i]; i++) { + struct commit *commit = NULL; + + if (!wt[i]->is_bare && !is_null_oid(&wt[i]->head_oid)) + commit = lookup_commit_reference_gently(repo, + &wt[i]->head_oid, 1); + if (!commit) { + free_worktree(wt[i]); + continue; + } + ALLOC_GROW(tips, nr + 1, alloc); + tips[nr] = commit; + wt[nr++] = wt[i]; + } + wt[nr] = NULL; + + CALLOC_ARRAY(merged, nr); + CALLOC_ARRAY(unmerged, nr); + if (filter->reachable_from) + mark_reachable(repo, tips, nr, filter->reachable_from, merged); + if (filter->unreachable_from) + mark_reachable(repo, tips, nr, filter->unreachable_from, + unmerged); + + for (size_t i = 0; i < nr; i++) { + if ((!filter->reachable_from || merged[i]) && !unmerged[i]) + wt[kept++] = wt[i]; + else + free_worktree(wt[i]); + } + wt[kept] = NULL; + + free(tips); + free(merged); + free(unmerged); +} + static int list(int ac, const char **av, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { int porcelain = 0; int line_terminator = '\n'; + struct ref_filter filter = REF_FILTER_INIT; struct option options[] = { OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), @@ -1138,6 +1208,8 @@ static int list(int ac, const char **av, const char *prefix, N_("add 'prunable' annotation to missing worktrees older than