Git development
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
	"D. Ben Knoble" <ben.knoble@gmail.com>,
	Phillip Wood <phillip.wood123@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v2 0/2] branch: let --delete-merged find squash merged branches
Date: Sun, 11 Oct 2026 09:41:03 +0000	[thread overview]
Message-ID: <pull.2425.v2.git.git.1791711665.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2425.git.git.1790667030497.gitgitgadget@gmail.com>

Branches merged on GitHub with "Squash and merge" or "Rebase and merge" are
never deleted by git branch --delete-merged, because their tips are not
reachable from the upstream. This treats such a branch as merged when some
upstream commit contains all of its changes, and names that commit in the
output:

Deleted branch topic (was 1a2b3c4, landed as 9f8e7d6).


Changes in v2:

 * Diff each upstream commit once and share the result between all branches,
   faster than v1.
 * Fix a branch merged with a merge commit being reported as "landed as" the
   merge when another branch was searched first, and add a test for it. Show
   the topic commit itself in "landed as" for work that arrived through a
   merge.
 * Use git merge --squash as the example in the commit message and mention
   GitHub's squash and rebase buttons.
 * Add --no-squashed to skip the squash search.

Harald Nordgren (2):
  branch: let --delete-merged find squash merged branches
  branch: add --no-squashed to skip the squash search

 Documentation/git-branch.adoc |  23 ++-
 builtin/branch.c              | 342 ++++++++++++++++++++++++++++++++--
 t/t3200-branch.sh             | 170 +++++++++++++++++
 3 files changed, 509 insertions(+), 26 deletions(-)


base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2425%2FHaraldNordgren%2Fbranch-delete-squashed-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2425/HaraldNordgren/branch-delete-squashed-v2
Pull-Request: https://github.com/git/git/pull/2425

Range-diff vs v1:

 1:  c9ab56d8b2 ! 1:  94a6d23f8b branch: let --delete-merged find squash merged branches
     @@ Metadata
       ## Commit message ##
          branch: let --delete-merged find squash merged branches
      
     -    Branches merged on GitHub with "Squash and merge" or "Rebase and
     -    merge" are never deleted by "git branch --delete-merged". The upstream
     -    holds a rewritten copy of their work, so their tips are not reachable
     -    from it and they look unmerged forever.
     +    A branch that upstream integrated with
     +
     +        git merge --squash topic
     +        git commit
     +
     +    is never deleted by "git branch --delete-merged". The same goes for a
     +    branch that was rebased or cherry-picked onto upstream. The upstream
     +    holds a rewritten copy of its work, so its tip is not reachable from
     +    it and it looks unmerged forever.
     +
     +    This happens all the time on hosting services. The "Squash and merge"
     +    and "Rebase and merge" buttons on GitHub land pull requests this way,
     +    so contributors to such projects collect local branches that
     +    "--delete-merged" never cleans up.
      
          Treat such a branch as merged when some upstream commit since the fork
          point contains all of its changes, so that merging the branch into
     @@ builtin/branch.c
       #include "help.h"
       #include "advice.h"
       #include "commit-reach.h"
     ++#include "commit-slab.h"
      +#include "diff.h"
      +#include "diffcore.h"
      +#include "hex.h"
     @@ builtin/branch.c: static int branch_pushes_to_upstream(struct branch *branch,
       	return ret;
       }
       
     ++enum change_state {
     ++	CHANGE_MISSING,
     ++	CHANGE_LANDED,
     ++	CHANGE_NEEDS_MERGE,
     ++};
     ++
      +struct branch_change {
      +	char *path;
     -+	struct object_id base_oid, branch_oid;
     ++	struct object_id base_oid, branch_oid, upstream_oid;
      +	unsigned short branch_mode;
     ++	enum change_state state;
     ++};
     ++
     ++struct landing_search {
     ++	const char *branch_name;
     ++	struct commit *rev;
     ++	struct commit *upstream;
     ++	struct branch_change *changes;
     ++	size_t changes_nr, changes_alloc;
      +};
      +
     -+static void collect_branch_changes(struct commit *base, struct commit *rev,
     -+				   struct branch_change **changes,
     -+				   size_t *nr, size_t *alloc)
     ++define_commit_slab(changed_paths, struct string_list *);
     ++
     ++static void collect_branch_changes(struct landing_search *search,
     ++				   struct commit *base)
      +{
      +	struct diff_options opt;
      +
     @@ builtin/branch.c: static int branch_pushes_to_upstream(struct branch *branch,
      +	opt.flags.recursive = 1;
      +	opt.output_format = DIFF_FORMAT_NO_OUTPUT;
      +	diff_setup_done(&opt);
     -+	diff_tree_oid(get_commit_tree_oid(base), get_commit_tree_oid(rev),
     -+		      "", &opt);
     ++	diff_tree_oid(get_commit_tree_oid(base),
     ++		      get_commit_tree_oid(search->rev), "", &opt);
      +	for (int i = 0; i < diff_queued_diff.nr; i++) {
      +		struct diff_filepair *p = diff_queued_diff.queue[i];
      +		struct branch_change *change;
      +
     -+		ALLOC_GROW(*changes, *nr + 1, *alloc);
     -+		change = &(*changes)[(*nr)++];
     ++		ALLOC_GROW(search->changes, search->changes_nr + 1,
     ++			   search->changes_alloc);
     ++		change = &search->changes[search->changes_nr++];
      +		change->path = xstrdup(p->two->path);
      +		oidcpy(&change->base_oid, DIFF_FILE_VALID(p->one) ?
      +		       &p->one->oid : null_oid(the_hash_algo));
     @@ builtin/branch.c: static int branch_pushes_to_upstream(struct branch *branch,
      +	diff_flush(&opt);
      +}
      +
     -+static int merge_keeps_upstream(const struct branch_change *change,
     -+				const struct object_id *upstream_oid)
     ++static int merge_keeps_upstream(const struct branch_change *change)
      +{
      +	mmfile_t base, upstream, branch;
      +	mmbuffer_t result = { 0 };
      +	int ret;
      +
      +	read_mmblob(&base, the_repository->objects, &change->base_oid);
     -+	read_mmblob(&upstream, the_repository->objects, upstream_oid);
     ++	read_mmblob(&upstream, the_repository->objects, &change->upstream_oid);
      +	read_mmblob(&branch, the_repository->objects, &change->branch_oid);
      +	ret = ll_merge(&result, change->path, &base, "base",
      +		       &upstream, "upstream", &branch, "branch",
     @@ builtin/branch.c: static int branch_pushes_to_upstream(struct branch *branch,
      +	return ret;
      +}
      +
     -+static int change_landed(const struct branch_change *change,
     -+			 struct commit *commit)
     ++static enum change_state change_state_at(struct branch_change *change,
     ++					 struct commit *commit)
      +{
     -+	struct object_id oid;
      +	unsigned short mode;
      +
      +	if (get_tree_entry(the_repository, get_commit_tree_oid(commit),
     -+			   change->path, &oid, &mode))
     -+		return is_null_oid(&change->branch_oid);
     -+	if (oideq(&oid, &change->branch_oid))
     -+		return mode == change->branch_mode;
     ++			   change->path, &change->upstream_oid, &mode))
     ++		return is_null_oid(&change->branch_oid) ?
     ++			CHANGE_LANDED : CHANGE_MISSING;
     ++	if (oideq(&change->upstream_oid, &change->branch_oid))
     ++		return mode == change->branch_mode ?
     ++			CHANGE_LANDED : CHANGE_MISSING;
      +	if (is_null_oid(&change->base_oid) ||
      +	    is_null_oid(&change->branch_oid) ||
     -+	    oideq(&oid, &change->base_oid) ||
     ++	    oideq(&change->upstream_oid, &change->base_oid) ||
      +	    mode != change->branch_mode || !S_ISREG(mode))
     -+		return 0;
     -+	return merge_keeps_upstream(change, &oid);
     ++		return CHANGE_MISSING;
     ++	return CHANGE_NEEDS_MERGE;
      +}
      +
     -+static struct commit *find_landed_commit(struct commit *rev,
     -+					 struct commit *upstream)
     ++static int changes_landed(struct landing_search *search,
     ++			  struct commit *commit)
     ++{
     ++	for (size_t i = 0; i < search->changes_nr; i++) {
     ++		struct branch_change *change = &search->changes[i];
     ++
     ++		change->state = change_state_at(change, commit);
     ++		if (change->state == CHANGE_MISSING)
     ++			return 0;
     ++	}
     ++	for (size_t i = 0; i < search->changes_nr; i++) {
     ++		struct branch_change *change = &search->changes[i];
     ++
     ++		if (change->state == CHANGE_NEEDS_MERGE &&
     ++		    !merge_keeps_upstream(change))
     ++			return 0;
     ++	}
     ++	return 1;
     ++}
     ++
     ++static struct string_list *changed_paths_of(struct changed_paths *cache,
     ++					    struct diff_options *opt,
     ++					    struct commit *commit)
     ++{
     ++	struct string_list **paths = changed_paths_at(cache, commit);
     ++	struct commit *parent;
     ++
     ++	if (*paths)
     ++		return *paths;
     ++
     ++	CALLOC_ARRAY(*paths, 1);
     ++	string_list_init_dup(*paths);
     ++	parent = commit->parents ? commit->parents->item : NULL;
     ++	if (parent && repo_parse_commit(the_repository, parent))
     ++		die(_("could not parse commit %s"),
     ++		    oid_to_hex(&parent->object.oid));
     ++	diff_tree_oid(parent ? get_commit_tree_oid(parent) : NULL,
     ++		      get_commit_tree_oid(commit), "", opt);
     ++	for (int i = 0; i < diff_queued_diff.nr; i++) {
     ++		struct diff_filepair *p = diff_queued_diff.queue[i];
     ++
     ++		string_list_append(*paths, p->two->path);
     ++	}
     ++	diff_flush(opt);
     ++	string_list_sort(*paths);
     ++	return *paths;
     ++}
     ++
     ++static void free_changed_paths(struct string_list **paths)
     ++{
     ++	if (!*paths)
     ++		return;
     ++	string_list_clear(*paths, 0);
     ++	free(*paths);
     ++}
     ++
     ++static int touches_changes(struct string_list *paths,
     ++			   const struct landing_search *search)
     ++{
     ++	for (size_t i = 0; i < search->changes_nr; i++)
     ++		if (string_list_has_string(paths, search->changes[i].path))
     ++			return 1;
     ++	return 0;
     ++}
     ++
     ++static struct commit *find_landed_commit(struct landing_search *search,
     ++					 struct changed_paths *cache,
     ++					 struct diff_options *opt)
      +{
     -+	struct commit_list *merge_bases = NULL;
     -+	struct branch_change *changes = NULL;
     -+	size_t changes_nr = 0, changes_alloc = 0;
      +	struct commit *commit, *landed = NULL;
      +	struct strvec args = STRVEC_INIT;
      +	struct rev_info revs;
      +
     -+	if (repo_get_merge_bases(the_repository, upstream, rev,
     -+				 &merge_bases) < 0)
     -+		exit(128);
     -+	if (!merge_bases)
     -+		return NULL;
     -+	collect_branch_changes(merge_bases->item, rev, &changes,
     -+			       &changes_nr, &changes_alloc);
     -+	commit_list_free(merge_bases);
     -+	if (!changes_nr)
     -+		return NULL;
     -+
      +	strvec_pushl(&args, "rev-list", "--reverse",
     -+		     oid_to_hex(&upstream->object.oid), NULL);
     -+	strvec_pushf(&args, "^%s", oid_to_hex(&rev->object.oid));
     -+	strvec_push(&args, "--");
     -+	for (size_t i = 0; i < changes_nr; i++)
     -+		strvec_pushf(&args, ":(literal)%s", changes[i].path);
     ++		     oid_to_hex(&search->upstream->object.oid), NULL);
     ++	strvec_pushf(&args, "^%s", oid_to_hex(&search->rev->object.oid));
      +
      +	repo_init_revisions(the_repository, &revs, NULL);
      +	setup_revisions_from_strvec(&args, &revs, NULL);
      +	if (prepare_revision_walk(&revs))
      +		die(_("revision walk setup failed"));
     -+	while (!landed && (commit = get_revision(&revs))) {
     -+		size_t i;
     -+
     -+		for (i = 0; i < changes_nr; i++)
     -+			if (!change_landed(&changes[i], commit))
     -+				break;
     -+		if (i == changes_nr)
     ++	while (!landed && (commit = get_revision(&revs)))
     ++		if (touches_changes(changed_paths_of(cache, opt, commit),
     ++				    search) &&
     ++		    changes_landed(search, commit))
      +			landed = commit;
     -+	}
      +	release_revisions(&revs);
     -+	clear_commit_marks(upstream, ALL_REV_FLAGS);
     -+	clear_commit_marks(rev, ALL_REV_FLAGS);
     ++	clear_commit_marks(search->upstream, ALL_REV_FLAGS);
     ++	clear_commit_marks(search->rev, ALL_REV_FLAGS);
      +	strvec_clear(&args);
     -+
     -+	for (size_t i = 0; i < changes_nr; i++)
     -+		free(changes[i].path);
     -+	free(changes);
      +	return landed;
      +}
     ++
     ++static void find_landed_commits(struct landing_search *searches,
     ++				size_t searches_nr,
     ++				struct strmap *landed_commits)
     ++{
     ++	struct changed_paths cache;
     ++	struct diff_options opt;
     ++
     ++	for (size_t i = 0; i < searches_nr; i++) {
     ++		struct landing_search *search = &searches[i];
     ++		struct commit_list *merge_bases = NULL;
     ++
     ++		if (repo_get_merge_bases(the_repository, search->upstream,
     ++					 search->rev, &merge_bases) < 0)
     ++			exit(128);
     ++		if (merge_bases)
     ++			collect_branch_changes(search, merge_bases->item);
     ++		commit_list_free(merge_bases);
     ++	}
     ++
     ++	repo_diff_setup(the_repository, &opt);
     ++	opt.flags.recursive = 1;
     ++	opt.output_format = DIFF_FORMAT_NO_OUTPUT;
     ++	opt.no_free = 1;
     ++	diff_setup_done(&opt);
     ++	init_changed_paths(&cache);
     ++
     ++	for (size_t i = 0; i < searches_nr; i++) {
     ++		struct commit *landed;
     ++
     ++		if (!searches[i].changes_nr)
     ++			continue;
     ++		landed = find_landed_commit(&searches[i], &cache, &opt);
     ++		if (landed)
     ++			strmap_put(landed_commits, searches[i].branch_name,
     ++				   landed);
     ++	}
     ++
     ++	deep_clear_changed_paths(&cache, free_changed_paths);
     ++	opt.no_free = 0;
     ++	diff_free(&opt);
     ++}
     ++
     ++static int branch_opted_out(struct strbuf *key, const char *branch_name,
     ++			    unsigned int flags)
     ++{
     ++	int opt_out;
     ++
     ++	strbuf_reset(key);
     ++	strbuf_addf(key, "branch.%s.deletemerged", branch_name);
     ++	if (repo_config_get_bool(the_repository, key->buf, &opt_out) ||
     ++	    opt_out)
     ++		return 0;
     ++	if (!(flags & DELETE_BRANCH_QUIET))
     ++		fprintf(stderr,
     ++			_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
     ++			branch_name, branch_name);
     ++	return 1;
     ++}
      +
       static int delete_merged_branches(const struct strvec *upstreams,
       				 const char **argv, unsigned int flags)
     @@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstrea
       	struct strset deletable_branch_names = STRSET_INIT;
       	struct strset protected_branch_names = STRSET_INIT;
      +	struct strmap landed_commits = STRMAP_INIT;
     ++	struct landing_search *searches = NULL;
     ++	size_t searches_nr = 0, searches_alloc = 0;
       	struct strvec branches_to_delete = STRVEC_INIT;
       	struct strbuf key = STRBUF_INIT;
       	struct hashmap_iter iter;
     @@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstrea
       		const char *branch_name;
       		struct branch *branch;
       		const char *upstream_refname;
     -+		struct commit *landed = NULL;
     - 		int opt_out;
     +-		int opt_out;
       
       		if (!skip_prefix(branch_refname, "refs/heads/", &branch_name))
     + 			BUG("filter returned non-branch ref '%s'", branch_refname);
      @@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
       			continue;
       		if (check_branch_commit(branch_name, branch_name,
       					&candidates.items[i]->objectname, NULL,
      -					FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED))
      -			continue;
     +-
     +-		strbuf_reset(&key);
     +-		strbuf_addf(&key, "branch.%s.deletemerged", branch_name);
     +-		if (!repo_config_get_bool(the_repository, key.buf, &opt_out) &&
     +-		    !opt_out) {
     +-			if (!(flags & DELETE_BRANCH_QUIET))
     +-				fprintf(stderr,
     +-					_("Skipping '%s' (branch.%s.deleteMerged is false)\n"),
     +-					branch_name, branch_name);
      +					FILTER_REFS_BRANCHES,
      +					DELETE_BRANCH_SKIP_UNMERGED)) {
      +			struct commit *rev = lookup_commit_reference(
     @@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstrea
      +			struct commit *upstream = lookup_commit_reference_by_name(
      +				upstream_refname);
      +
     -+			if (!rev || !upstream ||
     -+			    !(landed = find_landed_commit(rev, upstream)))
     ++			if (!rev || !upstream)
      +				continue;
     -+		}
     - 
     - 		strbuf_reset(&key);
     - 		strbuf_addf(&key, "branch.%s.deletemerged", branch_name);
     -@@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstreams,
     ++			ALLOC_GROW(searches, searches_nr + 1, searches_alloc);
     ++			searches[searches_nr++] = (struct landing_search) {
     ++				.branch_name = branch_name,
     ++				.rev = rev,
     ++				.upstream = upstream,
     ++			};
     + 			continue;
       		}
       
     - 		strset_add(&deletable_branch_names, branch_name);
     -+		if (landed)
     -+			strmap_put(&landed_commits, branch_name, landed);
     +-		strset_add(&deletable_branch_names, branch_name);
     ++		if (!branch_opted_out(&key, branch_name, flags))
     ++			strset_add(&deletable_branch_names, branch_name);
     ++	}
     ++
     ++	find_landed_commits(searches, searches_nr, &landed_commits);
     ++	for (size_t i = 0; i < searches_nr; i++) {
     ++		const char *branch_name = searches[i].branch_name;
     ++
     ++		if (strmap_contains(&landed_commits, branch_name) &&
     ++		    !branch_opted_out(&key, branch_name, flags))
     ++			strset_add(&deletable_branch_names, branch_name);
       	}
       
       	protect_stacked_branch_bases(refs, &deletable_branch_names,
     @@ builtin/branch.c: static int delete_merged_branches(const struct strvec *upstrea
       
       	strbuf_release(&key);
       	strvec_clear(&branches_to_delete);
     ++	for (size_t i = 0; i < searches_nr; i++) {
     ++		for (size_t j = 0; j < searches[i].changes_nr; j++)
     ++			free(searches[i].changes[j].path);
     ++		free(searches[i].changes);
     ++	}
     ++	free(searches);
      +	strmap_clear(&landed_commits, 0);
       	strset_clear(&protected_branch_names);
       	strset_clear(&deletable_branch_names);
     @@ t/t3200-branch.sh: test_expect_success '--delete-merged deletes only selected me
      +test_expect_success '--delete-merged deletes a squash merged branch' '
      +	setup_repo_for_delete_merged &&
      +	push_topic squashed squashed-one squashed-two &&
     -+	push_topic partial partial-landed partial-pending &&
     ++	push_topic partial lands-upstream stays-local &&
      +	squash_merge_upstream partial~1 &&
      +	squash_merge_upstream squashed &&
      +	squash=$(git -C upstream rev-parse --short next) &&
     @@ t/t3200-branch.sh: test_expect_success '--delete-merged deletes only selected me
      +		EOF
      +	)
      +'
     ++
     ++test_expect_success '--delete-merged deletes a squash merged branch after upstream changed the same file' '
     ++	(
     ++		cd upstream &&
     ++		git checkout next &&
     ++		test_write_lines 1 2 3 4 5 6 7 8 9 10 >shared &&
     ++		git add shared &&
     ++		git commit -m "add shared" &&
     ++		git checkout main
     ++	) &&
     ++	setup_repo_for_delete_merged &&
     ++	(
     ++		cd repo &&
     ++		git checkout -b edits-shared --track origin/next &&
     ++		test_write_lines 1 two 3 4 5 6 7 8 9 10 >shared &&
     ++		git commit -a -m "edit line 2" &&
     ++		git push origin edits-shared &&
     ++		git checkout --detach
     ++	) &&
     ++	(
     ++		cd upstream &&
     ++		git checkout next &&
     ++		test_write_lines 1 2 3 4 five 6 7 8 9 10 >shared &&
     ++		git commit -a -m "edit line 5" &&
     ++		git checkout main
     ++	) &&
     ++	squash_merge_upstream edits-shared &&
     ++	squash=$(git -C upstream rev-parse --short next) &&
     ++	(
     ++		cd repo &&
     ++		git fetch origin &&
     ++		sha=$(git rev-parse --short edits-shared) &&
     ++
     ++		git branch --delete-merged origin/next >actual 2>&1 &&
     ++		echo "Deleted branch edits-shared (was $sha, landed as $squash)." >expect &&
     ++		test_cmp expect actual &&
     ++
     ++		check_branches <<-\EOF
     ++		main
     ++		EOF
     ++	)
     ++'
     ++
     ++test_expect_success '--delete-merged reports a merge plainly next to an unmerged branch' '
     ++	setup_repo_for_delete_merged &&
     ++	push_topic a-unmerged unmerged-work &&
     ++	push_topic b-merged merged-work &&
     ++	(
     ++		cd upstream &&
     ++		git checkout next &&
     ++		test_commit upstream-work &&
     ++		git merge --no-ff b-merged &&
     ++		git checkout main
     ++	) &&
     ++	(
     ++		cd repo &&
     ++		git fetch origin &&
     ++		sha=$(git rev-parse --short b-merged) &&
     ++
     ++		git branch --delete-merged origin/next >actual 2>&1 &&
     ++		echo "Deleted branch b-merged (was $sha)." >expect &&
     ++		test_cmp expect actual &&
     ++
     ++		check_branches <<-\EOF
     ++		a-unmerged
     ++		main
     ++		EOF
     ++	)
     ++'
      +
       test_expect_success '--delete-merged keeps main despite a different default push remote' '
       	setup_repo_for_delete_merged &&
 -:  ---------- > 2:  ec88bde252 branch: add --no-squashed to skip the squash search

-- 
gitgitgadget

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

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-29  7:30 [PATCH] branch: let --delete-merged find squash merged branches Harald Nordgren via GitGitGadget
2026-09-29  7:46 ` Kristoffer Haugsbakk
2026-09-29  7:52   ` Harald Nordgren
2026-09-29  8:08     ` Kristoffer Haugsbakk
2026-09-29 11:18       ` D. Ben Knoble
2026-09-29 13:31         ` Kristoffer Haugsbakk
2026-09-29 11:26 ` D. Ben Knoble
2026-09-29 13:44   ` Harald Nordgren
2026-09-29 13:52   ` Harald Nordgren
2026-09-29 15:28     ` D. Ben Knoble
2026-09-29 18:34       ` Harald Nordgren
2026-10-04  9:54   ` Phillip Wood
2026-10-04 22:28     ` Harald Nordgren
2026-10-08 15:25       ` Phillip Wood
2026-10-08 18:42         ` Harald Nordgren
2026-10-09  8:16           ` Kristoffer Haugsbakk
2026-10-09 11:11             ` Harald Nordgren
2026-10-09 11:18           ` Harald Nordgren
2026-10-09 13:40             ` Phillip Wood
2026-10-09 13:37           ` Phillip Wood
2026-10-09 15:27             ` Harald Nordgren
2026-10-08 17:51     ` D. Ben Knoble
2026-10-09 13:34       ` Phillip Wood
2026-10-11  9:41 ` Harald Nordgren via GitGitGadget [this message]
2026-10-11  9:41   ` [PATCH v2 1/2] " Harald Nordgren via GitGitGadget
2026-10-11  9:41   ` [PATCH v2 2/2] branch: add --no-squashed to skip the squash search Harald Nordgren via GitGitGadget

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=pull.2425.v2.git.git.1791711665.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=ben.knoble@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    --cc=kristofferhaugsbakk@fastmail.com \
    --cc=phillip.wood123@gmail.com \
    /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