Git development
 help / color / mirror / Atom feed
* Re: [PATCH v2 8/9] refs: add peeled object ID to the `ref_update` struct
From: Toon Claes @ 2026-04-24 16:44 UTC (permalink / raw)
  To: Karthik Nayak, git; +Cc: Karthik Nayak, gitster, ps
In-Reply-To: <20260423-refs-move-to-generic-layer-v2-8-ae5a4f146d7d@gmail.com>

Karthik Nayak <karthik.188@gmail.com> writes:

> Certain reference backend {packed, reftable}, have the ability to also

Shouldn't it be:

  Certain reference backends {packed, reftable} have the ability to also

> store the peeled object ID for a reference pointing to a tag object.
> This has the added benefit that during retrieval of such references, we
> also obtain the peeled object ID without having to use the ODB.
>
> To provide this functionality, each backend independently calls the ODB
> to obtain the peeled OID. To move this functionality to the generic
> layer, there must be support infrastructure to pass in a peeled OID for
> reference updates.
>
> Add a `peeled` field to the `ref_update` structure and modify
> `ref_transaction_add_update()` to receive and copy this object ID to the
> `ref_update` structure. Finally, modify `ref_transaction_update()` to
> peel tag objects and pass the peeled OID to
> `ref_transaction_add_update()`.
>
> Update all callers of these functions with the new function parameters.
> Callers which only add reflog updates, need to only pass in NULL, since
> for reflogs, we don't store peeled OIDs. Reference deletions also only
> need to pass in NULL. For others, pass along the peeled OID if
> available.
>
> In a following commit, we'll modify the backends to use this peeled OID
> instead of parsing it themselves.
>
> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
> ---
>  refs.c                  | 15 +++++++++++++--
>  refs/files-backend.c    | 20 ++++++++++++--------
>  refs/refs-internal.h    | 14 ++++++++++++++
>  refs/reftable-backend.c |  6 +++---
>  4 files changed, 42 insertions(+), 13 deletions(-)
>
> diff --git a/refs/refs-internal.h b/refs/refs-internal.h
> index d103387ebf..307dcb277b 100644
> --- a/refs/refs-internal.h
> +++ b/refs/refs-internal.h
> @@ -39,6 +39,13 @@ struct ref_transaction;
>   */
>  #define REF_LOG_ONLY (1 << 7)
>  
> +/*
> + * The reference contains a peeled object ID. This is used when the
> + * new_oid is pointing to a tag object and the reference backend
> + * wants to also store the peeled value for optimized retrieval.
> + */
> +#define REF_HAVE_PEELED (1 << 15)

How did you end up picking this value?

I did some grepping to figure out if it would conflict with anything:

    git grep -h '#define REF_' -- '*.h' '*.c' |
      awk '/0x/{n=strtonum($3);b=0;while(n>1){n/=2;b++};$3="(1 << "b")"} 1' |
      sort -t'<' -k3 -n |
      column -t

(Yeah I got some help from AI to write the `awk` command)

Resulting in:

    #define  REF_EXCLUSIONS_INIT                   {                \
    #define  REF_FILTER_H
    #define  REF_FILTER_INIT                       {                \
    #define  REF_FORMAT_INIT                       {                \
    #define  REF_FORMATTING_STATE_INIT             {                0   }
    #define  REF_NO_DEREF                          (1               <<  0)
    #define  REF_NORMAL                            (1u              <<  0)
    #define  REF_STATES_INIT                       {                \
    #define  REF_STORE_ALL_CAPS                    (REF_STORE_READ  |   \
    #define  REF_STORE_CREATE_ON_DISK_IS_WORKTREE  (1               <<  0)
    #define  REF_STORE_READ                        (1               <<  0)
    #define  REF_TRANSACTION_UPDATE_ALLOWED_FLAGS  \
    #define  REF_BRANCHES                          (1u              <<  1)
    #define  REF_FORCE_CREATE_REFLOG               (1               <<  1)
    #define  REF_STORE_WRITE                       (1               <<  1)   /*  can  perform  update  operations  */
    #define  REF_HAVE_NEW                          (1               <<  2)
    #define  REF_STORE_ODB                         (1               <<  2)   /*  has  access   to      object      database  */
    #define  REF_TAGS                              (1u              <<  2)
    #define  REF_HAVE_OLD                          (1               <<  3)
    #define  REF_STORE_MAIN                        (1               <<  3)
    #define  REF_DIR                               (1               <<  4)
    #define  REF_IS_PRUNING                        (1               <<  4)
    #define  REF_DELETING                          (1               <<  5)
    #define  REF_INCOMPLETE                        (1               <<  5)
    #define  REF_KNOWS_PEELED                      (1               <<  6)
    #define  REF_NEEDS_COMMIT                      (1               <<  6)
    #define  REF_LOG_ONLY                          (1               <<  7)
    #define  REF_UPDATE_VIA_HEAD                   (1               <<  8)
    #define  REF_UPDATE_VIA_HEAD                   (1               <<  8)
    #define  REF_DELETED_RMDIR                     (1               <<  9)
    #define  REF_SKIP_OID_VERIFICATION             (1               <<  10)
    #define  REF_SKIP_REFNAME_VERIFICATION         (1               <<  11)
    #define  REF_SKIP_CREATE_REFLOG                (1               <<  12)
    #define  REF_LOG_USE_PROVIDED_OIDS             (1               <<  13)
    #define  REF_LOG_VIA_SPLIT                     (1               <<  14)
    #define  REF_HAVE_PEELED                       (1               <<  15)

So I guess it makes sense to use `(1 << 15)`.

-- 
Cheers,
Toon

^ permalink raw reply

* [PATCH] dir: use per-worktree repository ignore patterns upon request
From: D. Ben Knoble @ 2026-04-24 17:09 UTC (permalink / raw)
  To: git
  Cc: D. Ben Knoble, brian m . carlson, Patrick Steinhardt, Taylor Blau,
	Caleb White, Calvin Wan, Junio C Hamano, Elijah Newren,
	Andrew Berry, Jeff King, Derrick Stolee

Today we have $GIT_DIR/info/exclude for the main worktree, but $(git
rev-parse --git-dir)/info/exclude for secondary worktrees does not
actually contribute to ignore specs; instead, secondary worktrees also
use $GIT_COMMON_DIR/info/exclude.

Some users may prefer each worktree use its own ignore file; some may
prefer both; some may prefer the current behavior.

Add, test, and document extensions.worktreeIgnore that controls which
set of ignore files to use for worktrees.

Signed-off-by: D. Ben Knoble <ben.knoble+github@gmail.com>
---

Notes (benknoble/commits):
    Discussed briefly at https://lore.kernel.org/git/CALnO6CCXmA+ATT7CuyWkU6P8qmLCCpMi5Ppr1c78s0heznpVyw@mail.gmail.com/T

    This is based on next (4f69b47b94 (Merge branch 'ps/test-set-e-clean' into
    next, 2026-04-23)) but cleanly applies to master (94f057755b (Git 2.54,
    2026-04-19)) and seen (50541634cb (Merge branch
    'js/parseopt-subcommand-autocorrection' into seen, 2026-04-23)).

 Documentation/config/extensions.adoc |  7 ++++
 Documentation/git-worktree.adoc      |  3 ++
 Documentation/gitignore.adoc         |  6 ++--
 dir.c                                | 48 +++++++++++++++++++++++---
 t/meson.build                        |  1 +
 t/t2408-worktree-ignore.sh           | 50 ++++++++++++++++++++++++++++
 6 files changed, 109 insertions(+), 6 deletions(-)
 create mode 100755 t/t2408-worktree-ignore.sh

diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc
index be6678bb5b..5bfb06a54b 100644
--- a/Documentation/config/extensions.adoc
+++ b/Documentation/config/extensions.adoc
@@ -148,3 +148,10 @@ details.
 +
 For historical reasons, this extension is respected regardless of the
 `core.repositoryFormatVersion` setting.
+
+worktreeIgnore:::
+	If enabled, then worktrees will load per-repository ignore files from
+	`$GIT_DIR/info/exclude` (that is,
+	`$GIT_COMMON_DIR/worktrees/<id>/info/exclude`). If set to `merge`, then
+	both `$GIT_COMMON_DIR/info/exclude` and `$GIT_DIR/info/exclude` are
+	used.
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index fbf8426cd9..611c1e06b0 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -412,6 +412,9 @@ linkgit:gitrepository-layout[5] for details.
 When `extensions.worktreeConfig` is enabled, the config file
 `.git/worktrees/<id>/config.worktree` is read after `.git/config` is.
 
+See `extensions.worktreeIgnore` in linkgit:git-config[1] to control how
+per-repository ignore files are found in worktrees.
+
 LIST OUTPUT FORMAT
 ------------------
 The `worktree list` command has two output formats. The default format shows the
diff --git a/Documentation/gitignore.adoc b/Documentation/gitignore.adoc
index a3d24e5c34..d6976a44e4 100644
--- a/Documentation/gitignore.adoc
+++ b/Documentation/gitignore.adoc
@@ -7,7 +7,7 @@ gitignore - Specifies intentionally untracked files to ignore
 
 SYNOPSIS
 --------
-$XDG_CONFIG_HOME/git/ignore, $GIT_DIR/info/exclude, .gitignore
+$XDG_CONFIG_HOME/git/ignore, $GIT_COMMON_DIR/info/exclude, .gitignore
 
 DESCRIPTION
 -----------
@@ -34,7 +34,9 @@ precedence, the last matching pattern decides the outcome):
    includes such `.gitignore` files in its repository, containing patterns for
    files generated as part of the project build.
 
- * Patterns read from `$GIT_DIR/info/exclude`.
+ * Patterns read from `$GIT_COMMON_DIR/info/exclude`. (See
+   `extensions.worktreeIgnore` in linkgit:git-config[1] to change how this
+   applies to worktrees.)
 
  * Patterns read from the file specified by the configuration
    variable `core.excludesFile`.
diff --git a/dir.c b/dir.c
index fcb8f6dd2a..9592eb0062 100644
--- a/dir.c
+++ b/dir.c
@@ -36,6 +36,7 @@
 #include "trace2.h"
 #include "tree.h"
 #include "hex.h"
+#include "worktree.h"
 
  /*
   * The maximum size of a pattern/exclude file. If the file exceeds this size
@@ -3478,6 +3479,23 @@ int remove_dir_recursively(struct strbuf *path, int flag)
 }
 
 static GIT_PATH_FUNC(git_path_info_exclude, "info/exclude")
+static const char *git_worktree_info_exclude(void)
+{
+	static const char *ret;
+	if (!ret)
+	{
+		const struct worktree *wt = get_worktree_from_repository(the_repository);
+		ret = worktree_git_path(wt, "info/exclude");
+	}
+	return ret;
+}
+
+static void standard_exclude_from_info(const char *path, struct dir_struct *dir)
+{
+	struct oid_stat *oid_stat = dir->untracked ? &dir->internal.ss_info_exclude : NULL;
+	if (!access_or_warn(path, R_OK, 0))
+		add_patterns_from_file_1(dir, path, oid_stat);
+}
 
 void setup_standard_excludes(struct dir_struct *dir)
 {
@@ -3492,10 +3510,32 @@ void setup_standard_excludes(struct dir_struct *dir)
 
 	/* per repository user preference */
 	if (startup_info->have_repository) {
-		const char *path = git_path_info_exclude();
-		if (!access_or_warn(path, R_OK, 0))
-			add_patterns_from_file_1(dir, path,
-						 dir->untracked ? &dir->internal.ss_info_exclude : NULL);
+		/* extensions.worktreeIgnore determines which includes we add */
+		const char *wt_ignore;
+		int do_wt_ignore;
+		if (repo_config_get_value(the_repository,
+					  "extensions.worktreeIgnore",
+					  &wt_ignore)) {
+			/* unset: use main/.git/info/exclude */
+			standard_exclude_from_info(git_path_info_exclude(), dir);
+			return;
+		}
+		do_wt_ignore = git_parse_maybe_bool(wt_ignore);
+		if (!do_wt_ignore) {
+			/* false: as above */
+			standard_exclude_from_info(git_path_info_exclude(), dir);
+		} else if (do_wt_ignore > 0 || !strcmp(wt_ignore, "worktree")) {
+			/* true/worktree: use worktree/.git‡/info/exclude
+			 * ‡: resolving .git */
+			standard_exclude_from_info(git_worktree_info_exclude(), dir);
+		} else if (!strcmp(wt_ignore, "merge")) {
+			/* merge both! worktree last */
+			standard_exclude_from_info(git_path_info_exclude(), dir);
+			standard_exclude_from_info(git_worktree_info_exclude(), dir);
+		} else {
+			die(_("invalid value for '%s': '%s'"),
+			      "extensions.worktreeIgnore", wt_ignore);
+		}
 	}
 }
 
diff --git a/t/meson.build b/t/meson.build
index 7528e5cda5..30624b59e8 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -307,6 +307,7 @@ integration_tests = [
   't2405-worktree-submodule.sh',
   't2406-worktree-repair.sh',
   't2407-worktree-heads.sh',
+  't2408-worktree-ignore.sh',
   't2500-untracked-overwriting.sh',
   't2501-cwd-empty.sh',
   't3000-ls-files-others.sh',
diff --git a/t/t2408-worktree-ignore.sh b/t/t2408-worktree-ignore.sh
new file mode 100755
index 0000000000..67644ddd8d
--- /dev/null
+++ b/t/t2408-worktree-ignore.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='extensions.worktreeIgnore'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit init &&
+	git worktree add wt &&
+	echo main >.git/info/exclude &&
+	echo contents >main &&
+	echo contents >wt/main &&
+	wt_exclude="$(git -C wt rev-parse --git-dir)"/info/exclude &&
+	mkdir -p "$(dirname "$wt_exclude")" &&
+	echo worktree >"$wt_exclude" &&
+	echo contents >worktree &&
+	echo contents >wt/worktree
+'
+
+test_ignore_main() {
+	git check-ignore main &&
+	git -C wt check-ignore main
+}
+
+test_expect_success 'ignores main items by default' '
+	test_ignore_main
+'
+
+test_expect_success 'ignores main items with extensions.worktreeIgnore=no' '
+	test_config extensions.worktreeIgnore no &&
+	test_ignore_main
+'
+
+test_expect_success 'ignores worktree items with extensions.worktreeIgnore=worktree' '
+	test_config extensions.worktreeIgnore worktree &&
+	git check-ignore main &&
+	! git check-ignore worktree &&
+	! git -C wt check-ignore main &&
+	git -C wt check-ignore worktree
+'
+
+test_expect_success 'ignores all items with extensions.worktreeIgnore=merge' '
+	test_config extensions.worktreeIgnore merge &&
+	git check-ignore main &&
+	! git check-ignore worktree &&
+	git -C wt check-ignore main &&
+	git -C wt check-ignore worktree
+'
+
+test_done

base-commit: 4f69b47b940100b02630f745a52f9d9850f122b2
-- 
2.54.0.rc2.544.gc7ae2d5bb8.dirty


^ permalink raw reply related

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: D. Ben Knoble @ 2026-04-24 17:12 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren
In-Reply-To: <pull.2281.git.git.1777024991531.gitgitgadget@gmail.com>

On Fri, Apr 24, 2026 at 6:08 AM Harald Nordgren via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Add a --fetch option to git checkout and git switch, plus a
> checkout.autoFetch config to enable it by default. When set and the
> start-point argument names a configured remote (either bare, like
> "origin", or prefixed, like "origin/foo"), fetch that remote before
> resolving the ref. Aborts the checkout if the fetch fails.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>     checkout: add --fetch to fetch remote before resolving start-point
>
>     A workflow I run several times a day looks like:
>
>     git fetch origin
>     git checkout -b new_branch origin/some-branch
>
>
>     The first command exists purely to make the second one see an up-to-date
>     view of the remote. If I forget it, origin/some-branch points at a stale
>     commit, and I end up creating a local branch from the wrong starting
>     point.

When you realize this, "git pull --rebase" should help correct it.

>
>     This series teaches git checkout (and git switch) a new --fetch flag
>     that folds the two steps into one:
>
>     git checkout --fetch -b new_branch origin/some-branch
>
>
>     When the start-point argument names a configured remote — either bare
>     (origin, which resolves to the remote's default branch) or in / form —
>     git fetch is run before the start-point is resolved. If the fetch fails,
>     the checkout aborts and no local branch is created.
>
>     A new checkout.autoFetch config option enables the same behavior by
>     default, for users who always want it.

I could certainly see this being convenient. (I don't have any comment
on the code at this time.)

^ permalink raw reply

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Kristoffer Haugsbakk @ 2026-04-24 17:38 UTC (permalink / raw)
  To: git, gitgitgadget; +Cc: Harald Nordgren
In-Reply-To: <pull.2281.git.git.1777024991531.gitgitgadget@gmail.com>

On Fri, Apr 24, 2026, at 12:03, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Add a --fetch option to git checkout and git switch, plus a
> checkout.autoFetch config to enable it by default. When set and the

Why is the config not `checkout.config`? So it’s named the same as the
option (modulo snake case/camel case which is not relevant here).

> start-point argument names a configured remote (either bare, like
> "origin", or prefixed, like "origin/foo"),

It’s great that it only fetches when you have a remote-tracking branch
or alias for `<remote>/HEAD`. Doing a fetch on every <start-point> would
have been bad.

> fetch that remote before
> resolving the ref. Aborts the checkout if the fetch fails.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>     checkout: add --fetch to fetch remote before resolving start-point
>
>     A workflow I run several times a day looks like:
>
>     git fetch origin
>     git checkout -b new_branch origin/some-branch
>
>
>     The first command exists purely to make the second one see an up-to-date
>     view of the remote. If I forget it, origin/some-branch points at a stale
>     commit, and I end up creating a local branch from the wrong starting
>     point.
>
>     This series teaches git checkout (and git switch) a new --fetch flag
>     that folds the two steps into one:
>
>     git checkout --fetch -b new_branch origin/some-branch

The motivation for why this is being proposed maybe might as well go in
the commit message. Maybe that’s just me.

The commit message just says that “this thing is added”. Not why.

>
>
>     When the start-point argument names a configured remote — either bare
>     (origin, which resolves to the remote's default branch) or in / form —
>     git fetch is run before the start-point is resolved. If the fetch fails,
>     the checkout aborts and no local branch is created.
>
>     A new checkout.autoFetch config option enables the same behavior by
>     default, for users who always want it.
>
> Published-As:
> https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git
> pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v1
> Pull-Request: https://github.com/git/git/pull/2281
>
>  builtin/checkout.c    | 48 ++++++++++++++++++++++++++++++++++++++--
>  t/t7201-co.sh         | 51 +++++++++++++++++++++++++++++++++++++++++++
>  t/t9902-completion.sh |  1 +
>  3 files changed, 98 insertions(+), 2 deletions(-)

I guess a later version will have the changes to the documentation.

>
> diff --git a/builtin/checkout.c b/builtin/checkout.c
>[snip]
>  		argv += n;
>  		argc -= n;
>  	} else if (!opts->accept_ref && opts->from_treeish) {
> @@ -2052,6 +2092,8 @@ int cmd_checkout(int argc,
>  		OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode
> (default)")),
>  		OPT_BOOL(0, "auto-advance", &opts.auto_advance,
>  			 N_("auto advance to the next file when selecting hunks
> interactively")),
> +		OPT_BOOL(0, "fetch", &opts.auto_fetch,
> +			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),

s/remote-tracking ref/remote-tracking branch/ ?

git(1) doesn’t have a namespace for tracking refs in general.

>  		OPT_END()
>  	};
>
> @@ -2102,6 +2144,8 @@ int cmd_switch(int argc,
>  			 N_("second guess 'git switch <no-such-branch>'")),
>  		OPT_BOOL(0, "discard-changes", &opts.discard_changes,
>  			 N_("throw away local modifications")),
> +		OPT_BOOL(0, "fetch", &opts.auto_fetch,
> +			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),

Ditto.

>  		OPT_END()
>  	};
>[snip]

^ permalink raw reply

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Marc Branchaud @ 2026-04-24 17:42 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget, git; +Cc: Harald Nordgren
In-Reply-To: <pull.2281.git.git.1777024991531.gitgitgadget@gmail.com>


On 2026-04-24 04:03, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <haraldnordgren@gmail.com>
> 
> Add a --fetch option to git checkout and git switch, plus a
> checkout.autoFetch config to enable it by default. When set and the
> start-point argument names a configured remote (either bare, like
> "origin", or prefixed, like "origin/foo"), fetch that remote before
> resolving the ref. Aborts the checkout if the fetch fails.

Why tie the behaviour to the nature of the start-point?  That seems 
over-designed and prone to tripping people up.  Are you trying to cater 
to users who have multiple remotes?

I can imagine people who just want to do a checkout of anything after 
fetching -- maybe they want to checkout a new tag, or some other 
detached HEAD, or just an already-existing local branch.  They see that 
checkout has this nifty --fetch option so they think they can combine
	git fetch; git checkout
into a single command ... but no, only if they checkout something in a 
remote's namespace.

I don't personally feel the need for this new option, but I think you'll 
have a much easier time implementing and maintaining it if you just make 
--fetch do a plain fetch without caring about what the starting-point is.

		M.


> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
>      checkout: add --fetch to fetch remote before resolving start-point
>      
>      A workflow I run several times a day looks like:
>      
>      git fetch origin
>      git checkout -b new_branch origin/some-branch
>      
>      
>      The first command exists purely to make the second one see an up-to-date
>      view of the remote. If I forget it, origin/some-branch points at a stale
>      commit, and I end up creating a local branch from the wrong starting
>      point.
>      
>      This series teaches git checkout (and git switch) a new --fetch flag
>      that folds the two steps into one:
>      
>      git checkout --fetch -b new_branch origin/some-branch
>      
>      
>      When the start-point argument names a configured remote — either bare
>      (origin, which resolves to the remote's default branch) or in / form —
>      git fetch is run before the start-point is resolved. If the fetch fails,
>      the checkout aborts and no local branch is created.
>      
>      A new checkout.autoFetch config option enables the same behavior by
>      default, for users who always want it.
> 
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2281%2FHaraldNordgren%2Fcheckout-fetch-start-point-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2281/HaraldNordgren/checkout-fetch-start-point-v1
> Pull-Request: https://github.com/git/git/pull/2281
> 
>   builtin/checkout.c    | 48 ++++++++++++++++++++++++++++++++++++++--
>   t/t7201-co.sh         | 51 +++++++++++++++++++++++++++++++++++++++++++
>   t/t9902-completion.sh |  1 +
>   3 files changed, 98 insertions(+), 2 deletions(-)
> 
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index e031e61886..c8fbc4923b 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -30,7 +30,9 @@
>   #include "repo-settings.h"
>   #include "resolve-undo.h"
>   #include "revision.h"
> +#include "run-command.h"
>   #include "setup.h"
> +#include "strvec.h"
>   #include "submodule.h"
>   #include "symlinks.h"
>   #include "trace2.h"
> @@ -61,6 +63,7 @@ struct checkout_opts {
>   	int count_checkout_paths;
>   	int overlay_mode;
>   	int dwim_new_local_branch;
> +	int auto_fetch;
>   	int discard_changes;
>   	int accept_ref;
>   	int accept_pathspec;
> @@ -112,6 +115,34 @@ struct branch_info {
>   	char *checkout;
>   };
>   
> +static void fetch_remote_for_start_point(const char *arg)
> +{
> +	const char *slash;
> +	char *remote_name;
> +	struct remote *remote;
> +	struct child_process cmd = CHILD_PROCESS_INIT;
> +
> +	if (!arg || !*arg)
> +		return;
> +
> +	slash = strchr(arg, '/');
> +	if (slash == arg)
> +		return;
> +	remote_name = slash ? xstrndup(arg, slash - arg) : xstrdup(arg);
> +
> +	remote = remote_get(remote_name);
> +	if (!remote || !remote_is_configured(remote, 1)) {
> +		free(remote_name);
> +		return;
> +	}
> +
> +	strvec_pushl(&cmd.args, "fetch", remote_name, NULL);
> +	cmd.git_cmd = 1;
> +	free(remote_name);
> +	if (run_command(&cmd))
> +		die(_("failed to fetch start-point '%s'"), arg);
> +}
> +
>   static void branch_info_release(struct branch_info *info)
>   {
>   	free(info->name);
> @@ -1237,6 +1268,10 @@ static int git_checkout_config(const char *var, const char *value,
>   		opts->dwim_new_local_branch = git_config_bool(var, value);
>   		return 0;
>   	}
> +	if (!strcmp(var, "checkout.autofetch")) {
> +		opts->auto_fetch = git_config_bool(var, value);
> +		return 0;
> +	}
>   
>   	if (starts_with(var, "submodule."))
>   		return git_default_submodule_config(var, value, NULL);
> @@ -1942,8 +1977,13 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
>   			opts->dwim_new_local_branch &&
>   			opts->track == BRANCH_TRACK_UNSPECIFIED &&
>   			!opts->new_branch;
> -		int n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
> -					     &new_branch_info, opts, &rev);
> +		int n;
> +
> +		if (opts->auto_fetch)
> +			fetch_remote_for_start_point(argv[0]);
> +
> +		n = parse_branchname_arg(argc, argv, dwim_ok, which_command,
> +					 &new_branch_info, opts, &rev);
>   		argv += n;
>   		argc -= n;
>   	} else if (!opts->accept_ref && opts->from_treeish) {
> @@ -2052,6 +2092,8 @@ int cmd_checkout(int argc,
>   		OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")),
>   		OPT_BOOL(0, "auto-advance", &opts.auto_advance,
>   			 N_("auto advance to the next file when selecting hunks interactively")),
> +		OPT_BOOL(0, "fetch", &opts.auto_fetch,
> +			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),
>   		OPT_END()
>   	};
>   
> @@ -2102,6 +2144,8 @@ int cmd_switch(int argc,
>   			 N_("second guess 'git switch <no-such-branch>'")),
>   		OPT_BOOL(0, "discard-changes", &opts.discard_changes,
>   			 N_("throw away local modifications")),
> +		OPT_BOOL(0, "fetch", &opts.auto_fetch,
> +			 N_("fetch from the remote first if <start-point> is a remote-tracking ref")),
>   		OPT_END()
>   	};
>   
> diff --git a/t/t7201-co.sh b/t/t7201-co.sh
> index 9bcf7c0b40..60ddebd9c3 100755
> --- a/t/t7201-co.sh
> +++ b/t/t7201-co.sh
> @@ -801,4 +801,55 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
>   	test_cmp_config "" --default "" branch.main2.merge
>   '
>   
> +test_expect_success 'setup upstream for --fetch tests' '
> +	git checkout main &&
> +	git init fetch_upstream &&
> +	test_commit -C fetch_upstream u_main &&
> +	git remote add fetch_upstream fetch_upstream &&
> +	git fetch fetch_upstream &&
> +	git -C fetch_upstream checkout -b fetch_new &&
> +	test_commit -C fetch_upstream u_new
> +'
> +
> +test_expect_success 'checkout --fetch -b picks up branch created upstream after clone' '
> +	git checkout main &&
> +	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new &&
> +	git checkout --fetch -b local_new fetch_upstream/fetch_new &&
> +	test_cmp_rev refs/remotes/fetch_upstream/fetch_new HEAD
> +'
> +
> +test_expect_success 'checkout --fetch with bare remote name fetches the remote' '
> +	git checkout main &&
> +	git -C fetch_upstream checkout -b fetch_new2 &&
> +	test_commit -C fetch_upstream u_new2 &&
> +	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2 &&
> +	git checkout --fetch -b local_from_remote fetch_upstream &&
> +	git rev-parse --verify refs/remotes/fetch_upstream/fetch_new2
> +'
> +
> +test_expect_success 'checkout --fetch aborts and does not create branch on fetch failure' '
> +	git checkout main &&
> +	test_might_fail git branch -D bogus &&
> +	test_must_fail git checkout --fetch -b bogus fetch_upstream/does_not_exist &&
> +	test_must_fail git rev-parse --verify refs/heads/bogus
> +'
> +
> +test_expect_success 'checkout.autoFetch=true enables fetching without --fetch' '
> +	git checkout main &&
> +	git -C fetch_upstream checkout -b fetch_cfg &&
> +	test_commit -C fetch_upstream u_cfg &&
> +	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_cfg &&
> +	git -c checkout.autoFetch=true checkout -b local_cfg fetch_upstream/fetch_cfg &&
> +	test_cmp_rev refs/remotes/fetch_upstream/fetch_cfg HEAD
> +'
> +
> +test_expect_success 'switch --fetch -c picks up branch created upstream after clone' '
> +	git checkout main &&
> +	git -C fetch_upstream checkout -b fetch_switch &&
> +	test_commit -C fetch_upstream u_switch &&
> +	test_must_fail git rev-parse --verify refs/remotes/fetch_upstream/fetch_switch &&
> +	git switch --fetch -c local_switch fetch_upstream/fetch_switch &&
> +	test_cmp_rev refs/remotes/fetch_upstream/fetch_switch HEAD
> +'
> +
>   test_done
> diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
> index 2f9a597ec7..dc1d63669f 100755
> --- a/t/t9902-completion.sh
> +++ b/t/t9902-completion.sh
> @@ -2602,6 +2602,7 @@ test_expect_success 'double dash "git checkout"' '
>   	--ignore-other-worktrees Z
>   	--recurse-submodules Z
>   	--auto-advance Z
> +	--fetch Z
>   	--progress Z
>   	--guess Z
>   	--no-guess Z
> 
> base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0


^ permalink raw reply

* [PATCH] index-pack, unpack-objects: increase input buffer from 4 KiB to 128 KiB
From: Scott Bauersfeld via GitGitGadget @ 2026-04-24 19:14 UTC (permalink / raw)
  To: git; +Cc: Scott Bauersfeld, Scott Bauersfeld

From: Scott Bauersfeld <sbauersfeld@g.ucla.edu>

Both index-pack and unpack-objects read pack data from stdin through
a 4 KiB static buffer (input_buffer[4096]). On each fill(), consumed
bytes are flushed to the output pack file via write_or_die(), so
every write(2) moves at most 4 KiB.

On FUSE-backed filesystems every write(2) is a synchronous round
trip through the FUSE protocol (userspace -> kernel -> userspace ->
back), so the 4 KiB buffer turns a clone into many unnecessary tiny
writes with noticeable latency overhead.

Increase the buffer from 4 KiB to 128 KiB, matching the default
already used by the hashfile layer in csum-file.c.

Testing with strace on HTTPS clones of git/git (~296 MB pack, 5 runs
per variant, isolated builds from the same v2.54.0 source) shows:

  index-pack pack file writes: 72,465 -> 24,943 avg (66% reduction)
  total write() syscalls:     310,192 -> 259,530 avg (17% reduction)
  writes of exactly 4096 bytes: ~40,077 -> 0 (eliminated)

All clones produce identical HEAD, file count, and pass fsck.

Signed-off-by: Scott Bauersfeld <sbauersfeld@g.ucla.edu>
---
    index-pack, unpack-objects: increase input buffer from 4 KiB to 128 KiB
    
    Both index-pack and unpack-objects read pack data from stdin through a 4
    KiB static buffer (input_buffer[4096]). On each fill(), consumed bytes
    are flushed to the output pack file via write_or_die(), so every
    write(2) moves at most 4 KiB.
    
    On FUSE-backed filesystems every write(2) is a synchronous round trip
    through the FUSE protocol (userspace → kernel → userspace → back), so
    the 4 KiB buffer turns a clone into many unnecessary tiny writes with
    noticeable latency overhead.
    
    This change increase the buffer from 4 KiB to 128 KiB, matching the
    default already used by the hashfile layer in csum-file.c.
    
    Benchmarked with 5 HTTPS clones per version of
    https://github.com/sbauersfeld/git.git (~296 MB pack), using strace -f
    to count write() syscalls. Both binaries built from the same v2.54.0
    source tree in isolated directories to ensure the bin-wrappers resolve
    to the correct binary.
    
    Correctness verified via git fsck --no-dangling, rev-parse HEAD, and
    working tree file count — all 10 clones match.
    
    Results:
    
    Metric Unpatched (4 KiB) Patched (128 KiB) Change index-pack writes to
    pack file 72,465 avg 24,943 avg −66% Total write() syscalls (all
    processes) 310,192 avg 259,530 avg −17% Writes of exactly 4096 bytes
    ~40,077 avg 0 eliminated HEAD / file count / fsck ✓ ✓ None
    
    Raw data:
    
    unpatched (input_buffer[4096]): run 1: total_writes=311787
    ip_pack_writes=72353 ip_4k=35311 run 2: total_writes=310252
    ip_pack_writes=72348 ip_4k=38024 run 3: total_writes=309737
    ip_pack_writes=72303 ip_4k=43003 run 4: total_writes=309801
    ip_pack_writes=72661 ip_4k=42349 run 5: total_writes=309383
    ip_pack_writes=72662 ip_4k=41702
    
    patched (input_buffer[128 * 1024]): run 1: total_writes=264659
    ip_pack_writes=26605 ip_4k=0 run 2: total_writes=264276
    ip_pack_writes=26568 ip_4k=0 run 3: total_writes=227796 ip_pack_writes=
    9762 ip_4k=0 run 4: total_writes=262464 ip_pack_writes=27830 ip_4k=0 run
    5: total_writes=278455 ip_pack_writes=33952 ip_4k=0

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2282%2Fsbauersfeld%2Fsb%2Fincrease-index-pack-input-buffer-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2282/sbauersfeld/sb/increase-index-pack-input-buffer-v1
Pull-Request: https://github.com/git/git/pull/2282

 builtin/index-pack.c     | 4 ++--
 builtin/unpack-objects.c | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index ca7784dc2c..81a628bf34 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -145,8 +145,8 @@ static int check_self_contained_and_connected;
 
 static struct progress *progress;
 
-/* We always read in 4kB chunks. */
-static unsigned char input_buffer[4096];
+#define INPUT_BUFFER_SIZE (128 * 1024)
+static unsigned char input_buffer[INPUT_BUFFER_SIZE];
 static unsigned int input_offset, input_len;
 static off_t consumed_bytes;
 static off_t max_input_size;
diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c
index e01cf6e360..535c019f82 100644
--- a/builtin/unpack-objects.c
+++ b/builtin/unpack-objects.c
@@ -23,8 +23,8 @@
 static int dry_run, quiet, recover, has_errors, strict;
 static const char unpack_usage[] = "git unpack-objects [-n] [-q] [-r] [--strict]";
 
-/* We always read in 4kB chunks. */
-static unsigned char buffer[4096];
+#define INPUT_BUFFER_SIZE (128 * 1024)
+static unsigned char buffer[INPUT_BUFFER_SIZE];
 static unsigned int offset, len;
 static off_t consumed_bytes;
 static off_t max_input_size;

base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH] dir: use per-worktree repository ignore patterns upon request
From: brian m. carlson @ 2026-04-24 19:35 UTC (permalink / raw)
  To: D. Ben Knoble
  Cc: git, Patrick Steinhardt, Taylor Blau, Caleb White, Calvin Wan,
	Junio C Hamano, Elijah Newren, Andrew Berry, Jeff King,
	Derrick Stolee
In-Reply-To: <e3ee0a11b566dd2cc605447c111ae4620bce0fe6.1777050300.git.ben.knoble+github@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 1040 bytes --]

On 2026-04-24 at 17:09:19, D. Ben Knoble wrote:
> Today we have $GIT_DIR/info/exclude for the main worktree, but $(git
> rev-parse --git-dir)/info/exclude for secondary worktrees does not
> actually contribute to ignore specs; instead, secondary worktrees also
> use $GIT_COMMON_DIR/info/exclude.
> 
> Some users may prefer each worktree use its own ignore file; some may
> prefer both; some may prefer the current behavior.
> 
> Add, test, and document extensions.worktreeIgnore that controls which
> set of ignore files to use for worktrees.

I was wondering if we could make something like
`extensions.worktreeInfoFiles` or something and also make the
`info/attributes` (and maybe `info/sparse-checkout`) file used instead.
I think that would be somewhat better than having additional options
added on after the fact and be a little more generally applicable.

What do you think?  My apologies for not mentioning this sooner before
you'd already sent a patch.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply

* Re: [PATCH] dir: use per-worktree repository ignore patterns upon request
From: Phillip Wood @ 2026-04-24 19:53 UTC (permalink / raw)
  To: D. Ben Knoble, git
  Cc: brian m . carlson, Patrick Steinhardt, Taylor Blau, Caleb White,
	Calvin Wan, Junio C Hamano, Elijah Newren, Andrew Berry,
	Jeff King, Derrick Stolee
In-Reply-To: <e3ee0a11b566dd2cc605447c111ae4620bce0fe6.1777050300.git.ben.knoble+github@gmail.com>

Hi Ben

On 24/04/2026 18:09, D. Ben Knoble wrote:
> Today we have $GIT_DIR/info/exclude for the main worktree, but $(git
> rev-parse --git-dir)/info/exclude for secondary worktrees does not
> actually contribute to ignore specs; instead, secondary worktrees also
> use $GIT_COMMON_DIR/info/exclude.

Sharing the same set of exclude patterns between all worktrees seems 
like a reasonable thing to do - it would be a pain to have to populate 
them every time a new worktree was created.

	git rev-parse --git-path info/exclude

always returns the correct path because it uses repo_git_path() which 
knows whether a particular path is per-worktree or resides under 
$GIT_COMMON_DIR. It is best to avoid constructing paths manually using 
"git rev-parse --git-dir"

> Some users may prefer each worktree use its own ignore file; some may
> prefer both; some may prefer the current behavior.

This sounds quite hypothetical - do we have a concrete use case for 
per-worktree exclude files?

If there is a use for per-worktree excludes it would be much better to 
use a different path such as "info.worktree/exclude" so that the main 
worktree can have both shared and per-worktree excludes. We could use 
"exclude.worktree" like "config.worktree" but then we'd be in danger of 
adding new ".worktree" files in the future if we want to add 
per-worktree versions of other files that live under "info/"

Thanks

Phillip

> Add, test, and document extensions.worktreeIgnore that controls which
> set of ignore files to use for worktrees.
> 
> Signed-off-by: D. Ben Knoble <ben.knoble+github@gmail.com>
> ---
> 
> Notes (benknoble/commits):
>      Discussed briefly at https://lore.kernel.org/git/CALnO6CCXmA+ATT7CuyWkU6P8qmLCCpMi5Ppr1c78s0heznpVyw@mail.gmail.com/T
> 
>      This is based on next (4f69b47b94 (Merge branch 'ps/test-set-e-clean' into
>      next, 2026-04-23)) but cleanly applies to master (94f057755b (Git 2.54,
>      2026-04-19)) and seen (50541634cb (Merge branch
>      'js/parseopt-subcommand-autocorrection' into seen, 2026-04-23)).
> 
>   Documentation/config/extensions.adoc |  7 ++++
>   Documentation/git-worktree.adoc      |  3 ++
>   Documentation/gitignore.adoc         |  6 ++--
>   dir.c                                | 48 +++++++++++++++++++++++---
>   t/meson.build                        |  1 +
>   t/t2408-worktree-ignore.sh           | 50 ++++++++++++++++++++++++++++
>   6 files changed, 109 insertions(+), 6 deletions(-)
>   create mode 100755 t/t2408-worktree-ignore.sh
> 
> diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc
> index be6678bb5b..5bfb06a54b 100644
> --- a/Documentation/config/extensions.adoc
> +++ b/Documentation/config/extensions.adoc
> @@ -148,3 +148,10 @@ details.
>   +
>   For historical reasons, this extension is respected regardless of the
>   `core.repositoryFormatVersion` setting.
> +
> +worktreeIgnore:::
> +	If enabled, then worktrees will load per-repository ignore files from
> +	`$GIT_DIR/info/exclude` (that is,
> +	`$GIT_COMMON_DIR/worktrees/<id>/info/exclude`). If set to `merge`, then
> +	both `$GIT_COMMON_DIR/info/exclude` and `$GIT_DIR/info/exclude` are
> +	used.
> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
> index fbf8426cd9..611c1e06b0 100644
> --- a/Documentation/git-worktree.adoc
> +++ b/Documentation/git-worktree.adoc
> @@ -412,6 +412,9 @@ linkgit:gitrepository-layout[5] for details.
>   When `extensions.worktreeConfig` is enabled, the config file
>   `.git/worktrees/<id>/config.worktree` is read after `.git/config` is.
>   
> +See `extensions.worktreeIgnore` in linkgit:git-config[1] to control how
> +per-repository ignore files are found in worktrees.
> +
>   LIST OUTPUT FORMAT
>   ------------------
>   The `worktree list` command has two output formats. The default format shows the
> diff --git a/Documentation/gitignore.adoc b/Documentation/gitignore.adoc
> index a3d24e5c34..d6976a44e4 100644
> --- a/Documentation/gitignore.adoc
> +++ b/Documentation/gitignore.adoc
> @@ -7,7 +7,7 @@ gitignore - Specifies intentionally untracked files to ignore
>   
>   SYNOPSIS
>   --------
> -$XDG_CONFIG_HOME/git/ignore, $GIT_DIR/info/exclude, .gitignore
> +$XDG_CONFIG_HOME/git/ignore, $GIT_COMMON_DIR/info/exclude, .gitignore
>   
>   DESCRIPTION
>   -----------
> @@ -34,7 +34,9 @@ precedence, the last matching pattern decides the outcome):
>      includes such `.gitignore` files in its repository, containing patterns for
>      files generated as part of the project build.
>   
> - * Patterns read from `$GIT_DIR/info/exclude`.
> + * Patterns read from `$GIT_COMMON_DIR/info/exclude`. (See
> +   `extensions.worktreeIgnore` in linkgit:git-config[1] to change how this
> +   applies to worktrees.)
>   
>    * Patterns read from the file specified by the configuration
>      variable `core.excludesFile`.
> diff --git a/dir.c b/dir.c
> index fcb8f6dd2a..9592eb0062 100644
> --- a/dir.c
> +++ b/dir.c
> @@ -36,6 +36,7 @@
>   #include "trace2.h"
>   #include "tree.h"
>   #include "hex.h"
> +#include "worktree.h"
>   
>    /*
>     * The maximum size of a pattern/exclude file. If the file exceeds this size
> @@ -3478,6 +3479,23 @@ int remove_dir_recursively(struct strbuf *path, int flag)
>   }
>   
>   static GIT_PATH_FUNC(git_path_info_exclude, "info/exclude")
> +static const char *git_worktree_info_exclude(void)
> +{
> +	static const char *ret;
> +	if (!ret)
> +	{
> +		const struct worktree *wt = get_worktree_from_repository(the_repository);
> +		ret = worktree_git_path(wt, "info/exclude");
> +	}
> +	return ret;
> +}
> +
> +static void standard_exclude_from_info(const char *path, struct dir_struct *dir)
> +{
> +	struct oid_stat *oid_stat = dir->untracked ? &dir->internal.ss_info_exclude : NULL;
> +	if (!access_or_warn(path, R_OK, 0))
> +		add_patterns_from_file_1(dir, path, oid_stat);
> +}
>   
>   void setup_standard_excludes(struct dir_struct *dir)
>   {
> @@ -3492,10 +3510,32 @@ void setup_standard_excludes(struct dir_struct *dir)
>   
>   	/* per repository user preference */
>   	if (startup_info->have_repository) {
> -		const char *path = git_path_info_exclude();
> -		if (!access_or_warn(path, R_OK, 0))
> -			add_patterns_from_file_1(dir, path,
> -						 dir->untracked ? &dir->internal.ss_info_exclude : NULL);
> +		/* extensions.worktreeIgnore determines which includes we add */
> +		const char *wt_ignore;
> +		int do_wt_ignore;
> +		if (repo_config_get_value(the_repository,
> +					  "extensions.worktreeIgnore",
> +					  &wt_ignore)) {
> +			/* unset: use main/.git/info/exclude */
> +			standard_exclude_from_info(git_path_info_exclude(), dir);
> +			return;
> +		}
> +		do_wt_ignore = git_parse_maybe_bool(wt_ignore);
> +		if (!do_wt_ignore) {
> +			/* false: as above */
> +			standard_exclude_from_info(git_path_info_exclude(), dir);
> +		} else if (do_wt_ignore > 0 || !strcmp(wt_ignore, "worktree")) {
> +			/* true/worktree: use worktree/.git‡/info/exclude
> +			 * ‡: resolving .git */
> +			standard_exclude_from_info(git_worktree_info_exclude(), dir);
> +		} else if (!strcmp(wt_ignore, "merge")) {
> +			/* merge both! worktree last */
> +			standard_exclude_from_info(git_path_info_exclude(), dir);
> +			standard_exclude_from_info(git_worktree_info_exclude(), dir);
> +		} else {
> +			die(_("invalid value for '%s': '%s'"),
> +			      "extensions.worktreeIgnore", wt_ignore);
> +		}
>   	}
>   }
>   
> diff --git a/t/meson.build b/t/meson.build
> index 7528e5cda5..30624b59e8 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -307,6 +307,7 @@ integration_tests = [
>     't2405-worktree-submodule.sh',
>     't2406-worktree-repair.sh',
>     't2407-worktree-heads.sh',
> +  't2408-worktree-ignore.sh',
>     't2500-untracked-overwriting.sh',
>     't2501-cwd-empty.sh',
>     't3000-ls-files-others.sh',
> diff --git a/t/t2408-worktree-ignore.sh b/t/t2408-worktree-ignore.sh
> new file mode 100755
> index 0000000000..67644ddd8d
> --- /dev/null
> +++ b/t/t2408-worktree-ignore.sh
> @@ -0,0 +1,50 @@
> +#!/bin/sh
> +
> +test_description='extensions.worktreeIgnore'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'setup' '
> +	test_commit init &&
> +	git worktree add wt &&
> +	echo main >.git/info/exclude &&
> +	echo contents >main &&
> +	echo contents >wt/main &&
> +	wt_exclude="$(git -C wt rev-parse --git-dir)"/info/exclude &&
> +	mkdir -p "$(dirname "$wt_exclude")" &&
> +	echo worktree >"$wt_exclude" &&
> +	echo contents >worktree &&
> +	echo contents >wt/worktree
> +'
> +
> +test_ignore_main() {
> +	git check-ignore main &&
> +	git -C wt check-ignore main
> +}
> +
> +test_expect_success 'ignores main items by default' '
> +	test_ignore_main
> +'
> +
> +test_expect_success 'ignores main items with extensions.worktreeIgnore=no' '
> +	test_config extensions.worktreeIgnore no &&
> +	test_ignore_main
> +'
> +
> +test_expect_success 'ignores worktree items with extensions.worktreeIgnore=worktree' '
> +	test_config extensions.worktreeIgnore worktree &&
> +	git check-ignore main &&
> +	! git check-ignore worktree &&
> +	! git -C wt check-ignore main &&
> +	git -C wt check-ignore worktree
> +'
> +
> +test_expect_success 'ignores all items with extensions.worktreeIgnore=merge' '
> +	test_config extensions.worktreeIgnore merge &&
> +	git check-ignore main &&
> +	! git check-ignore worktree &&
> +	git -C wt check-ignore main &&
> +	git -C wt check-ignore worktree
> +'
> +
> +test_done
> 
> base-commit: 4f69b47b940100b02630f745a52f9d9850f122b2


^ permalink raw reply

* Re: [PATCH 2/2] commit: sign commit after mutating buffer
From: brian m. carlson @ 2026-04-24 20:17 UTC (permalink / raw)
  To: Elijah Newren; +Cc: git, Junio C Hamano, Kushal Das
In-Reply-To: <CABPp-BEd9saiMWVdcSaZBHqGreshpA=fGZc4AvYkoU=swSSuxA@mail.gmail.com>

[-- Attachment #1: Type: text/plain, Size: 1796 bytes --]

On 2026-04-22 at 15:10:29, Elijah Newren wrote:
> On Mon, Apr 20, 2026 at 3:14 PM brian m. carlson
> <sandals@crustytoothpaste.net> wrote:
> > diff --git a/commit.c b/commit.c
> > index 790dd2faed..bc41859be1 100644
> > --- a/commit.c
> > +++ b/commit.c
> > @@ -1747,6 +1747,11 @@ int commit_tree_extended(const char *msg, size_t msg_len,
> >                 oidcpy(&parent_buf[i++], &p->item->object.oid);
> >
> >         write_commit_tree(&buffer, msg, msg_len, tree, parent_buf, nparents, author, committer, extra);
> > +
> > +       /* And check the encoding. */
> > +       if (encoding_is_utf8 && !ensure_utf8(&buffer))
> > +               fprintf(stderr, _(commit_utf8_warn));
> > +
> >         if (sign_commit && sign_buffer(&buffer, &sig, sign_commit,
> >                                        SIGN_BUFFER_USE_DEFAULT_KEY)) {
> >                 result = -1;
> > @@ -1780,6 +1785,9 @@ int commit_tree_extended(const char *msg, size_t msg_len,
> >                 free_commit_extra_headers(compat_extra);
> >                 free(mapped_parents);
> >
> > +               if (encoding_is_utf8 && !ensure_utf8(&compat_buffer))
> > +                       fprintf(stderr, _(commit_utf8_warn));
> > +
> 
> So the users might see "commit message did not conform to UTF-8..."
> twice? (Isn't compat_buffer likely to have invalid UTF-8 whenever
> buffer does?)  Do we want to avoid that double printing?

Yeah, I'll fix that in v2.

> Did the change in this patch also fix a short-circuiting error?
> Previously, when both buffers had invalid UTF-8, we'd only call
> ensure_utf8() on the first one and fix it, and then short-circuit and
> not handle compat_buffer, right?

I believe it did, yes.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply

* Comments on Phillip's review
From: Harald Nordgren @ 2026-04-24 20:52 UTC (permalink / raw)
  To: phillip.wood123
  Cc: chris.torek, git, gitgitgadget, haraldnordgren, peff,
	phillip.wood
In-Reply-To: <65f77343-2ee6-4ed6-adb2-271814148310@gmail.com>

> ret is -1 so we return the same value if unpack_trees() fails as do the
> checks at the top of the function do when they fail with "return
> error(...)". Therefore we cannot determine whether a failure of this
> function is due to unpack_trees() or not and so we wont know whether to
> autostash or not. You need to return a unique value here like -2 (or
> ideally a named constant)

👍

> Good - now we only try to restore the stashed changes if we actually
> stashed. However we only restore the stashed changes if there was an
> error(). If there isn't an error we call update_refs_for_switch() before
> restoring them. It would be safer to restore them straight away in case
> that function ends up dying for any reason (though I think that's pretty
> unlikely)

I hope I understand correctly, the code becomes easier this way, so that's
nice!

> As I said last time we should not be calling apply_autostash() if we
> have not created an autostash. We should also not discard and re-read
> the index if we haven't stashed. I do think we'd be better restoring the
> stashed changes in a single place as I said above.

Makes sense.

> where the changes appear to be part of the advice message. Perhaps we
> should print a short (i.e. one sentance) message along the lines of
> 
>         The following paths have local changes
> 
> We should test what the user sees here as well.

Add that message.

Do you mean to test the full output? I'm not against it at all, but that
seems to be going against the convention of the other tests in this file.
But it would be a more robust test.

> I'm not sure we need to say "local changes" twice here

👍

> I find the bulleted list a bit odd, maybe
> 
>         You can either resolve the conflicts and then discard the stash
>         with "git stash drop", or, if you do not want to resolve them
>         now, run "git reset --hard" and apply the local changes later by
>         running "git stash pop"
> 
> would be better?

Much cleaner, thanks!

> we already have tests for --conflict=diff3 and
> --conflict=merge I'm not sure this test adds much.

Deleted.

> > +
> > +     cat <<-EOF >expect &&
> > +     a
> > +     <<<<<<< simple
> > +     c
> > +     ||||||| main
> > +     b
> > +     c
> > +     d
> > +     =======
> > +     b
> > +     X
> > +     d
> > +     >>>>>>> local
> > +     e
> > +     EOF
> > +     test_cmp expect two
> > +'
> > +
> > +test_expect_success 'checkout -m respects merge.conflictStyle config' '
> 
> Looking at the existing tests, 'checkout with --merge, in diff3 -m
> style' and 'checkout --conflict=merge, overriding config' already test
> that we respect merge.conflictStyle and that --conflict overrides it so
> I don't see what new coverage this test adds.

Deleted.

> > +test_expect_success 'checkout -m skips stash when no conflict' '
> > +     git checkout -f main &&
> > +     git clean -f &&
> > +
> > +     fill 0 x y z >same &&
> > +     git stash list >stash-before &&
> > +     git checkout -m side >actual 2>&1 &&
> 
> file "same" is unchanged between branch "side" and "branch" main so we
> do not need to stash it.

Deleted.

> > +     test_grep ! "Created autostash" actual &&
> > +     git stash list >stash-after &&
> > +     test_cmp stash-before stash-after &&
> > +     fill 0 x y z >expect &&
> > +     test_cmp expect same
> 
> Even if we created an autostash this test would not pick it up as the
> stash is not written to refs/stash unless there are merge conflicts and
> we don't print "Created autostash" even when we do create an autostash.
> The same is true for "checkout -m -b skips stash with dirty tree" below.
> I don't see how we can check that a stash was not created without using
> GIT_TRACE to see if we run "git stash". Even that is fragile as we might
> start stashing without forking a separate process in future.

Deleted.

> I don't think the two tests above add any extra coverage when we have
> the one below so they can be deleted. Our test suite is slow enough
> already - we only need one test to fail for any given issue.

Deleted.

> > +test_expect_success 'checkout -m stashes on truly conflicting changes' '
> 
> This use of conflicting is rather confusing - what's the difference
> between a conflicting change and a truly conflicting change?
> 
> I think a single test is sufficient to check that we create a valid
> stash entry

Updated.

> test_expect_success 'checkout -m which would overwrite untracked file' '
>         git checkout -f --detach main &&
>         test_commit another-file &&
>         git checkout HEAD^ &&
>         >another-file.t &&
>         test_must_fail git checkout -m @{-1} 2>err &&
>         test_grep "another-file.t.*overwritten" err
> '
> 
> which passes on master but fails with these patches applied. We need to
> make sure that we don't set "quiet" in unpack_tree_opts the second time
> we call merge_working_tree(). The test could be improved by adding some
> local changes.

Tricky to get right, the test if very good to have! I rewrote the logic now
to make this test pass, I hope it looks better now.


Harald

^ permalink raw reply

* [PATCH] grep: fix --column --only-match for 2nd and later matches
From: René Scharfe @ 2026-04-24 21:04 UTC (permalink / raw)
  To: Brandon Chinn, git
In-Reply-To: <CAGANf=dkRgFp+bEkB5f8QBeiR3m+3WE8sKqT9vKstkGHqbxA3A@mail.gmail.com>

"git grep --column --only-match" shows the 1-based column number of the
first match on each line, but confusing numbers for further matches.
Example:

   $ echo 123456789012345678901234567890 >file
   $ for d in 1 2 3 4 5 6 7 8 9 0
     do
       git grep --no-index --column --only-matching $d file |
       awk -v FS=: -v l=$d: '{l = l sprintf("%3s", $2)} END {print l}'
     done
   1:  1  2 12
   2:  2  4 14
   3:  3  6 16
   4:  4  8 18
   5:  5 10 20
   6:  6 12 22
   7:  7 14 24
   8:  8 16 26
   9:  9 18 28
   0: 10 20 30

Report the column number of each match instead:

   $ for d in 1 2 3 4 5 6 7 8 9 0
     do
       ./git grep --no-index --column --only-matching $d file |
       awk -v FS=: -v l=$d: '{l = l sprintf("%3s", $2)} END {print l}'
     done
   1:  1 11 21
   2:  2 12 22
   3:  3 13 23
   4:  4 14 24
   5:  5 15 25
   6:  6 16 26
   7:  7 17 27
   8:  8 18 28
   9:  9 19 29
   0: 10 20 30

We need to adjust the test in t7810 as well.  The file it uses has the
following five lines; I add a line highlighting the matches and a ruler
at the bottom here, to make it easier to see that the second "mmap"
indeed starts at column 14:

foo mmap bar
foo_mmap bar
foo_mmap bar mmap
foo mmap bar_mmap
foo_mmap bar mmap baz
    ====     ====
123456789 123456789 1

Reported-by: Brandon Chinn <brandonchinn178@gmail.com>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
 grep.c          | 3 ++-
 t/t7810-grep.sh | 6 +++---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/grep.c b/grep.c
index c7e1dc1e0e..a54e5d86a9 100644
--- a/grep.c
+++ b/grep.c
@@ -1267,6 +1267,7 @@ static void show_line(struct grep_opt *opt,
 		regmatch_t match;
 		enum grep_context ctx = GREP_CONTEXT_BODY;
 		int eflags = 0;
+		const char *start = bol;
 
 		if (want_color(opt->color)) {
 			if (sign == ':')
@@ -1285,6 +1286,7 @@ static void show_line(struct grep_opt *opt,
 			if (match.rm_so == match.rm_eo)
 				break;
 
+			cno = bol - start + match.rm_so + 1;
 			if (opt->only_matching)
 				show_line_header(opt, name, lno, cno, sign);
 			else
@@ -1294,7 +1296,6 @@ static void show_line(struct grep_opt *opt,
 			if (opt->only_matching)
 				opt->output(opt, "\n", 1);
 			bol += match.rm_eo;
-			cno += match.rm_eo;
 			rest -= match.rm_eo;
 			eflags = REG_NOTBOL;
 		}
diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh
index 64ac4f04ee..bd439563d6 100755
--- a/t/t7810-grep.sh
+++ b/t/t7810-grep.sh
@@ -322,11 +322,11 @@ do
 		${HC}file:1:5:mmap
 		${HC}file:2:5:mmap
 		${HC}file:3:5:mmap
-		${HC}file:3:13:mmap
+		${HC}file:3:14:mmap
 		${HC}file:4:5:mmap
-		${HC}file:4:13:mmap
+		${HC}file:4:14:mmap
 		${HC}file:5:5:mmap
-		${HC}file:5:13:mmap
+		${HC}file:5:14:mmap
 		EOF
 		git grep --column -n -o -e mmap $H >actual &&
 		test_cmp expected actual
-- 
2.54.0

^ permalink raw reply related

* [PATCH v15 0/5] checkout: 'autostash' for branch switching
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren
In-Reply-To: <pull.2234.v14.git.git.1776270259.gitgitgadget@gmail.com>

 * When the initial merge fails, the caller now knows for sure it was a
   conflict (not some unrelated error) before kicking off the autostash.

 * If the second attempt also fails, the user now actually sees why.
   Previously errors like "would be overwritten by checkout" were being
   swallowed.

 * The stash is reapplied right after the second attempt, so it won't get
   left behind if something else goes wrong later in the checkout.

 * A couple of clean-up steps only run when we actually stashed, so regular
   checkouts aren't paying for work they don't need.

 * Added a small header "The following paths have local changes:" before the
   list of changed files, so it doesn't look like part of the conflict
   advice above it.

 * Reworded the conflict advice into a single paragraph. Feels less shouty
   and doesn't say "local changes" three times.

 * Trimmed and consolidated tests in t7201. A bunch of the old ones couldn't
   actually fail, or were really testing other things; the conflict-path
   tests are now a single test that checks the stash entry directly.

Harald Nordgren (5):
  stash: add --label-ours, --label-theirs, --label-base for apply
  sequencer: allow create_autostash to run silently
  sequencer: teach autostash apply to take optional conflict marker
    labels
  checkout: rollback lock on early returns in merge_working_tree
  checkout -m: autostash when switching branches

 Documentation/git-checkout.adoc |  58 +++++------
 Documentation/git-stash.adoc    |  11 ++-
 Documentation/git-switch.adoc   |  33 +++----
 builtin/checkout.c              | 165 +++++++++++++++-----------------
 builtin/commit.c                |   3 +-
 builtin/merge.c                 |  15 ++-
 builtin/stash.c                 |  28 ++++--
 sequencer.c                     |  69 +++++++++----
 sequencer.h                     |   7 +-
 t/t3420-rebase-autostash.sh     |  16 ++--
 t/t3903-stash.sh                |  24 +++++
 t/t7201-co.sh                   |  61 +++++++++++-
 t/t7600-merge.sh                |   3 +-
 xdiff-interface.c               |  12 +++
 xdiff-interface.h               |   1 +
 xdiff/xmerge.c                  |   6 +-
 16 files changed, 335 insertions(+), 177 deletions(-)


base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2234%2FHaraldNordgren%2Fcheckout_autostash-v15
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2234/HaraldNordgren/checkout_autostash-v15
Pull-Request: https://github.com/git/git/pull/2234

Range-diff vs v14:

 1:  e18c25599a = 1:  aba8e6a9dc stash: add --label-ours, --label-theirs, --label-base for apply
 2:  ce29b10264 = 2:  89e0bfa803 sequencer: allow create_autostash to run silently
 3:  73051d1762 = 3:  a428ce7328 sequencer: teach autostash apply to take optional conflict marker labels
 4:  191058d8e3 = 4:  f358424085 checkout: rollback lock on early returns in merge_working_tree
 5:  86f33df1eb ! 5:  96b14db827 checkout -m: autostash when switching branches
     @@ builtin/checkout.c
       #include "setup.h"
       #include "submodule.h"
       #include "symlinks.h"
     +@@ builtin/checkout.c: struct checkout_opts {
     + 	.auto_advance = 1, \
     + }
     + 
     ++#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
     ++
     + struct branch_info {
     + 	char *name; /* The short name used */
     + 	char *path; /* The full name of a real branch */
     +@@ builtin/checkout.c: static void setup_branch_path(struct branch_info *branch)
     + 
     + static void init_topts(struct unpack_trees_options *topts, int merge,
     + 		       int show_progress, int overwrite_ignore,
     +-		       struct commit *old_commit)
     ++		       struct commit *old_commit, bool show_unpack_errors)
     + {
     + 	memset(topts, 0, sizeof(*topts));
     + 	topts->head_idx = -1;
     +@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge,
     + 	topts->initial_checkout = is_index_unborn(the_repository->index);
     + 	topts->update = 1;
     + 	topts->merge = 1;
     +-	topts->quiet = merge && old_commit;
     ++	topts->quiet = merge && old_commit && !show_unpack_errors;
     + 	topts->verbose_update = show_progress;
     + 	topts->fn = twoway_merge;
     + 	topts->preserve_ignored = !overwrite_ignore;
     +@@ builtin/checkout.c: static void init_topts(struct unpack_trees_options *topts, int merge,
     + static int merge_working_tree(const struct checkout_opts *opts,
     + 			      struct branch_info *old_branch_info,
     + 			      struct branch_info *new_branch_info,
     +-			      int *writeout_error)
     ++			      int *writeout_error,
     ++			      bool show_unpack_errors)
     + {
     + 	int ret;
     + 	struct lock_file lock_file = LOCK_INIT;
     +@@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
     + 
     + 		/* 2-way merge to the new branch */
     + 		init_topts(&topts, opts->merge, opts->show_progress,
     +-			   opts->overwrite_ignore, old_branch_info->commit);
     ++			   opts->overwrite_ignore, old_branch_info->commit,
     ++			   show_unpack_errors);
     + 		init_checkout_metadata(&topts.meta, new_branch_info->refname,
     + 				       new_branch_info->commit ?
     + 				       &new_branch_info->commit->object.oid :
      @@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *opts,
       		ret = unpack_trees(2, trees, &topts);
       		clear_unpack_trees_porcelain(&topts);
     @@ builtin/checkout.c: static int merge_working_tree(const struct checkout_opts *op
      -				return ret;
      -			}
      +			rollback_lock_file(&lock_file);
     -+			return ret;
     ++			return MERGE_WORKING_TREE_UNPACK_FAILED;
       		}
       	}
       
     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
       			do_merge = 0;
       	}
       
     -+	if (old_branch_info.name)
     ++	if (old_branch_info.name) {
      +		stash_label_base = old_branch_info.name;
     -+	else if (old_branch_info.commit) {
     ++	} else if (old_branch_info.commit) {
      +		strbuf_add_unique_abbrev(&old_commit_shortname,
      +					 &old_branch_info.commit->object.oid,
      +					 DEFAULT_ABBREV);
     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
      +	}
      +
       	if (do_merge) {
     - 		ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
     -+		if (ret == -1 && opts->merge) {
     +-		ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
     ++		ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
     ++					 &writeout_error, false);
     ++		if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
      +			strbuf_addf(&autostash_msg,
      +				    "autostash while switching to '%s'",
      +				    new_branch_info->name);
     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
      +					     "CHECKOUT_AUTOSTASH_HEAD",
      +					     autostash_msg.buf, true);
      +			created_autostash = 1;
     -+			ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
     ++			ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
     ++						 &writeout_error, true);
     ++		}
     ++		if (created_autostash) {
     ++			if (opts->conflict_style >= 0) {
     ++				struct strbuf cfg = STRBUF_INIT;
     ++				strbuf_addf(&cfg, "merge.conflictStyle=%s",
     ++					    conflict_style_name(opts->conflict_style));
     ++				git_config_push_parameter(cfg.buf);
     ++				strbuf_release(&cfg);
     ++			}
     ++			apply_autostash_ref(the_repository,
     ++					    "CHECKOUT_AUTOSTASH_HEAD",
     ++					    new_branch_info->name,
     ++					    "local",
     ++					    stash_label_base,
     ++					    autostash_msg.buf);
      +		}
       		if (ret) {
     -+			if (created_autostash)
     -+				apply_autostash_ref(the_repository,
     -+						    "CHECKOUT_AUTOSTASH_HEAD",
     -+						    new_branch_info->name,
     -+						    "local",
     -+						    stash_label_base,
     -+						    autostash_msg.buf);
       			branch_info_release(&old_branch_info);
      -			return ret;
      +			strbuf_release(&old_commit_shortname);
     @@ builtin/checkout.c: static int switch_branches(const struct checkout_opts *opts,
       
       	update_refs_for_switch(opts, &old_branch_info, new_branch_info);
       
     -+	if (opts->conflict_style >= 0) {
     -+		struct strbuf cfg = STRBUF_INIT;
     -+		strbuf_addf(&cfg, "merge.conflictStyle=%s",
     -+			    conflict_style_name(opts->conflict_style));
     -+		git_config_push_parameter(cfg.buf);
     -+		strbuf_release(&cfg);
     ++	if (created_autostash) {
     ++		discard_index(the_repository->index);
     ++		if (repo_read_index(the_repository) < 0)
     ++			die(_("index file corrupt"));
     ++
     ++		if (!opts->quiet && new_branch_info->commit) {
     ++			printf(_("The following paths have local changes:\n"));
     ++			show_local_changes(&new_branch_info->commit->object,
     ++					   &opts->diff_options);
     ++		}
      +	}
     -+	apply_autostash_ref(the_repository, "CHECKOUT_AUTOSTASH_HEAD",
     -+			    new_branch_info->name, "local",
     -+			    stash_label_base,
     -+			    autostash_msg.buf);
     -+
     -+	discard_index(the_repository->index);
     -+	if (repo_read_index(the_repository) < 0)
     -+		die(_("index file corrupt"));
     -+
     -+	if (created_autostash && !opts->quiet && new_branch_info->commit)
     -+		show_local_changes(&new_branch_info->commit->object,
     -+				   &opts->diff_options);
      +
       	ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
       	branch_info_release(&old_branch_info);
     @@ sequencer.c: static int apply_save_autostash_oid(const char *stash_oid, int atte
       			ret = error(_("cannot store %s"), stash_oid);
      +		else if (attempt_apply)
      +			fprintf(stderr,
     -+				_("Your local changes are stashed, however, applying it to carry\n"
     -+				  "forward your local changes resulted in conflicts:\n"
     -+				  "\n"
     -+				  " - You can try resolving them now.  If you resolved them\n"
     -+				  "   successfully, discard the stash entry with \"git stash drop\".\n"
     -+				  "\n"
     -+				  " - Alternatively you can \"git reset --hard\" if you do not want\n"
     -+				  "   to deal with them right now, and later \"git stash pop\" to\n"
     -+				  "   recover your local changes.\n"));
     ++				_("Your local changes are stashed, however applying them\n"
     ++				  "resulted in conflicts.  You can either resolve the conflicts\n"
     ++				  "and then discard the stash with \"git stash drop\", or, if you\n"
     ++				  "do not want to resolve them now, run \"git reset --hard\" and\n"
     ++				  "apply the local changes later by running \"git stash pop\".\n"));
       		else
       			fprintf(stderr,
      -				_("%s\n"
     @@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () {
      -	Applying autostash resulted in conflicts.
      -	Your changes are safe in the stash.
      -	You can run "git stash pop" or "git stash drop" at any time.
     -+	Your local changes are stashed, however, applying it to carry
     -+	forward your local changes resulted in conflicts:
     -+
     -+	 - You can try resolving them now.  If you resolved them
     -+	   successfully, discard the stash entry with "git stash drop".
     -+
     -+	 - Alternatively you can "git reset --hard" if you do not want
     -+	   to deal with them right now, and later "git stash pop" to
     -+	   recover your local changes.
     ++	Your local changes are stashed, however applying them
     ++	resulted in conflicts.  You can either resolve the conflicts
     ++	and then discard the stash with "git stash drop", or, if you
     ++	do not want to resolve them now, run "git reset --hard" and
     ++	apply the local changes later by running "git stash pop".
       	EOF
       }
       
     @@ t/t3420-rebase-autostash.sh: create_expected_failure_apply () {
      -	Applying autostash resulted in conflicts.
      -	Your changes are safe in the stash.
      -	You can run "git stash pop" or "git stash drop" at any time.
     -+	Your local changes are stashed, however, applying it to carry
     -+	forward your local changes resulted in conflicts:
     -+
     -+	 - You can try resolving them now.  If you resolved them
     -+	   successfully, discard the stash entry with "git stash drop".
     -+
     -+	 - Alternatively you can "git reset --hard" if you do not want
     -+	   to deal with them right now, and later "git stash pop" to
     -+	   recover your local changes.
     ++	Your local changes are stashed, however applying them
     ++	resulted in conflicts.  You can either resolve the conflicts
     ++	and then discard the stash with "git stash drop", or, if you
     ++	do not want to resolve them now, run "git reset --hard" and
     ++	apply the local changes later by running "git stash pop".
       	Successfully rebased and updated refs/heads/rebased-feature-branch.
       	EOF
       }
      
       ## t/t7201-co.sh ##
     +@@ t/t7201-co.sh: test_expect_success 'checkout -m with dirty tree' '
     + 
     + 	test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
     + 
     +-	printf "M\t%s\n" one >expect.messages &&
     ++	printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
     + 	test_cmp expect.messages messages &&
     + 
     + 	fill "M	one" "A	three" "D	two" >expect.main &&
      @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>' '
       	test_cmp expect two
       '
       
     -+test_expect_success 'checkout --merge --conflict=zdiff3 <branch>' '
     -+	git checkout -f main &&
     -+	git reset --hard &&
     -+	git clean -f &&
     -+
     -+	fill a b X d e >two &&
     -+	git checkout --merge --conflict=zdiff3 simple &&
     -+
     -+	cat <<-EOF >expect &&
     -+	a
     -+	<<<<<<< simple
     -+	c
     -+	||||||| main
     -+	b
     -+	c
     -+	d
     -+	=======
     -+	b
     -+	X
     -+	d
     -+	>>>>>>> local
     -+	e
     -+	EOF
     -+	test_cmp expect two
     -+'
     -+
     -+test_expect_success 'checkout -m respects merge.conflictStyle config' '
     -+	git checkout -f main &&
     -+	git reset --hard &&
     -+	git clean -f &&
     -+
     -+	test_config merge.conflictStyle diff3 &&
     -+	fill b d >two &&
     -+	git checkout -m simple &&
     -+
     -+	cat <<-EOF >expect &&
     -+	<<<<<<< simple
     -+	a
     -+	c
     -+	e
     -+	||||||| main
     -+	a
     -+	b
     -+	c
     -+	d
     -+	e
     -+	=======
     -+	b
     -+	d
     -+	>>>>>>> local
     -+	EOF
     -+	test_cmp expect two
     -+'
     -+
     -+test_expect_success 'checkout -m skips stash when no conflict' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 0 x y z >same &&
     -+	git stash list >stash-before &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
     -+	git stash list >stash-after &&
     -+	test_cmp stash-before stash-after &&
     -+	fill 0 x y z >expect &&
     -+	test_cmp expect same
     -+'
     -+
     -+test_expect_success 'checkout -m skips stash with non-conflicting dirty index' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 0 x y z >same &&
     -+	git add same &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
     -+	fill 0 x y z >expect &&
     -+	test_cmp expect same
     -+'
     -+
     -+test_expect_success 'checkout -m stashes and applies on conflicting changes' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 1 2 3 4 5 6 7 >one &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
     -+	test_grep "Applied autostash" actual &&
     -+	fill 1 2 3 4 5 6 7 >expect &&
     -+	test_cmp expect one
     -+'
     -+
      +test_expect_success 'checkout -m with mixed staged and unstaged changes' '
      +	git checkout -f main &&
      +	git clean -f &&
     @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
      +	git add same &&
      +	fill 1 2 3 4 5 6 7 >one &&
      +	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
      +	test_grep "Applied autostash" actual &&
      +	fill 0 x y z >expect &&
      +	test_cmp expect same &&
     @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
      +	test_cmp expect one
      +'
      +
     -+test_expect_success 'checkout -m stashes on truly conflicting changes' '
     ++test_expect_success 'checkout -m creates a recoverable stash on conflict' '
      +	git checkout -f main &&
      +	git clean -f &&
      +
     @@ t/t7201-co.sh: test_expect_success 'checkout --merge --conflict=diff3 <branch>'
      +	test_must_fail git checkout side 2>stderr &&
      +	test_grep "Your local changes" stderr &&
      +	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
      +	test_grep "resulted in conflicts" actual &&
      +	test_grep "git stash drop" actual &&
     ++	test_grep "git stash pop" actual &&
     ++	test_grep "The following paths have local changes" actual &&
     ++	git show --format=%B --diff-merges=1 refs/stash >actual &&
     ++	sed /^index/d actual >actual.trimmed &&
     ++	cat >expect <<-EOF &&
     ++	On main: autostash while switching to ${SQ}side${SQ}
     ++
     ++	diff --git a/one b/one
     ++	--- a/one
     ++	+++ b/one
     ++	@@ -3,6 +3,3 @@
     ++	 3
     ++	 4
     ++	 5
     ++	-6
     ++	-7
     ++	-8
     ++	EOF
     ++	test_cmp expect actual.trimmed &&
      +	git stash drop &&
      +	git reset --hard
      +'
      +
     -+test_expect_success 'checkout -m produces usable stash on conflict' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     ++test_expect_success 'checkout -m which would overwrite untracked file' '
     ++	git checkout -f --detach main &&
     ++	test_commit another-file &&
     ++	git checkout HEAD^ &&
     ++	>another-file.t &&
      +	fill 1 2 3 4 5 >one &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	test_grep "recover your local changes" actual &&
     -+	git checkout -f main &&
     -+	git stash pop &&
     -+	fill 1 2 3 4 5 >expect &&
     -+	test_cmp expect one
     -+'
     -+
     -+test_expect_success 'checkout -m autostash message includes target branch' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 1 2 3 4 5 >one &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	git stash list >stash-list &&
     -+	test_grep "autostash while switching to .side." stash-list &&
     -+	git stash drop &&
     -+	git checkout -f main &&
     -+	git reset --hard
     -+'
     -+
     -+test_expect_success 'checkout -m stashes on staged conflicting changes' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 1 2 3 4 5 >one &&
     -+	git add one &&
     -+	git checkout -m side >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
     -+	test_grep "resulted in conflicts" actual &&
     -+	test_grep "git stash drop" actual &&
     -+	git stash drop &&
     -+	git reset --hard
     -+'
     -+
     -+test_expect_success 'checkout -m applies stash cleanly with non-overlapping changes in same file' '
     -+	git checkout -f main &&
     -+	git reset --hard &&
     -+	git clean -f &&
     -+
     -+	git checkout -b nonoverlap_base &&
     -+	fill a b c d >file &&
     -+	git add file &&
     -+	git commit -m "add file" &&
     -+
     -+	git checkout -b nonoverlap_child &&
     -+	fill a b c INSERTED d >file &&
     -+	git commit -a -m "insert line near end of file" &&
     -+
     -+	fill DIRTY a b c INSERTED d >file &&
     -+
     -+	git stash list >stash-before &&
     -+	git checkout -m nonoverlap_base 2>stderr &&
     -+	test_grep "Applied autostash" stderr &&
     -+	test_grep ! "resulted in conflicts" stderr &&
     -+
     -+	git stash list >stash-after &&
     -+	test_cmp stash-before stash-after &&
     -+
     -+	fill DIRTY a b c d >expect &&
     -+	test_cmp expect file &&
     -+
     -+	git checkout -f main &&
     -+	git branch -D nonoverlap_base &&
     -+	git branch -D nonoverlap_child
     -+'
     -+
     -+test_expect_success 'checkout -m -b skips stash with dirty tree' '
     -+	git checkout -f main &&
     -+	git clean -f &&
     -+
     -+	fill 0 x y z >same &&
     -+	git checkout -m -b newbranch >actual 2>&1 &&
     -+	test_grep ! "Created autostash" actual &&
     -+	fill 0 x y z >expect &&
     -+	test_cmp expect same &&
     -+	git checkout main &&
     -+	git branch -D newbranch
     ++	test_must_fail git checkout -m @{-1} 2>err &&
     ++	test_grep "would be overwritten by checkout" err &&
     ++	test_grep "another-file.t" err
      +'
      +
       test_expect_success 'switch to another branch while carrying a deletion' '
     @@ t/t7600-merge.sh: test_expect_success 'merge with conflicted --autostash changes
       	test_when_finished "test_might_fail git stash drop" &&
       	git merge --autostash c3 2>err &&
      -	test_grep "Applying autostash resulted in conflicts." err &&
     -+	test_grep "your local changes resulted in conflicts" err &&
     ++	test_grep "applying them" err &&
     ++	test_grep "resulted in conflicts" err &&
       	git show HEAD:file >merge-result &&
       	test_cmp result.1-9 merge-result &&
       	git stash show -p >actual &&

-- 
gitgitgadget

^ permalink raw reply

* [PATCH v15 1/5] stash: add --label-ours, --label-theirs, --label-base for apply
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Allow callers of "git stash apply" to pass custom labels for conflict
markers instead of the default "Updated upstream" and "Stashed changes".
Document the new options and add a test.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-stash.adoc | 11 ++++++++++-
 builtin/stash.c              | 28 ++++++++++++++++++++--------
 t/t3903-stash.sh             | 24 ++++++++++++++++++++++++
 xdiff/xmerge.c               |  6 +++---
 4 files changed, 57 insertions(+), 12 deletions(-)

diff --git a/Documentation/git-stash.adoc b/Documentation/git-stash.adoc
index b05c990ecd..50bb89f483 100644
--- a/Documentation/git-stash.adoc
+++ b/Documentation/git-stash.adoc
@@ -12,7 +12,7 @@ git stash list [<log-options>]
 git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
 git stash drop [-q | --quiet] [<stash>]
 git stash pop [--index] [-q | --quiet] [<stash>]
-git stash apply [--index] [-q | --quiet] [<stash>]
+git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]
 git stash branch <branchname> [<stash>]
 git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
 	     [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts
 (which are stored in the index, where you therefore can no longer
 apply the changes as they were originally).
 
+`--label-ours=<label>`::
+`--label-theirs=<label>`::
+`--label-base=<label>`::
+	These options are only valid for the `apply` command.
++
+Use the given labels in conflict markers instead of the default
+"Updated upstream", "Stashed changes", and "Stash base".
+`--label-base` only has an effect with merge.conflictStyle=diff3.
+
 `-k`::
 `--keep-index`::
 `--no-keep-index`::
diff --git a/builtin/stash.c b/builtin/stash.c
index 0d27b2fb1f..32dbc97b47 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -44,7 +44,7 @@
 #define BUILTIN_STASH_POP_USAGE \
 	N_("git stash pop [--index] [-q | --quiet] [<stash>]")
 #define BUILTIN_STASH_APPLY_USAGE \
-	N_("git stash apply [--index] [-q | --quiet] [<stash>]")
+	N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")
 #define BUILTIN_STASH_BRANCH_USAGE \
 	N_("git stash branch <branchname> [<stash>]")
 #define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)
 }
 
 static int do_apply_stash(const char *prefix, struct stash_info *info,
-			  int index, int quiet)
+			  int index, int quiet,
+			  const char *label_ours, const char *label_theirs,
+			  const char *label_base)
 {
 	int clean, ret;
 	int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
 
 	init_ui_merge_options(&o, the_repository);
 
-	o.branch1 = "Updated upstream";
-	o.branch2 = "Stashed changes";
-	o.ancestor = "Stash base";
+	o.branch1 = label_ours ? label_ours : "Updated upstream";
+	o.branch2 = label_theirs ? label_theirs : "Stashed changes";
+	o.ancestor = label_base ? label_base : "Stash base";
 
 	if (oideq(&info->b_tree, &c_tree))
 		o.branch1 = "Version stash was based on";
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
 	int ret = -1;
 	int quiet = 0;
 	int index = use_index;
+	const char *label_ours = NULL, *label_theirs = NULL, *label_base = NULL;
 	struct stash_info info = STASH_INFO_INIT;
 	struct option options[] = {
 		OPT__QUIET(&quiet, N_("be quiet, only report errors")),
 		OPT_BOOL(0, "index", &index,
 			 N_("attempt to recreate the index")),
+		OPT_STRING(0, "label-ours", &label_ours, N_("label"),
+			   N_("label for the upstream side in conflict markers")),
+		OPT_STRING(0, "label-theirs", &label_theirs, N_("label"),
+			   N_("label for the stashed side in conflict markers")),
+		OPT_STRING(0, "label-base", &label_base, N_("label"),
+			   N_("label for the base in diff3 conflict markers")),
 		OPT_END()
 	};
 
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
 	if (get_stash_info(&info, argc, argv))
 		goto cleanup;
 
-	ret = do_apply_stash(prefix, &info, index, quiet);
+	ret = do_apply_stash(prefix, &info, index, quiet,
+			     label_ours, label_theirs, label_base);
 cleanup:
 	free_stash_info(&info);
 	return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,
 	if (get_stash_info_assert(&info, argc, argv))
 		goto cleanup;
 
-	if ((ret = do_apply_stash(prefix, &info, index, quiet)))
+	if ((ret = do_apply_stash(prefix, &info, index, quiet,
+				  NULL, NULL, NULL)))
 		printf_ln(_("The stash entry is kept in case "
 			    "you need it again."));
 	else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,
 	strvec_push(&cp.args, oid_to_hex(&info.b_commit));
 	ret = run_command(&cp);
 	if (!ret)
-		ret = do_apply_stash(prefix, &info, 1, 0);
+		ret = do_apply_stash(prefix, &info, 1, 0,
+				     NULL, NULL, NULL);
 	if (!ret && info.is_stash_ref)
 		ret = do_drop_stash(&info, 0);
 
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 70879941c2..bdaad22e1f 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -56,6 +56,7 @@ setup_stash() {
 	git add other-file &&
 	test_tick &&
 	git commit -m initial &&
+	git tag initial &&
 	echo 2 >file &&
 	git add file &&
 	echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' '
 	test_cmp expect file
 '
 
+test_expect_success 'apply with custom conflict labels' '
+	git reset --hard initial &&
+	test_commit label-base conflict-file base-content &&
+	echo stashed >conflict-file &&
+	git stash push -m "stashed" &&
+	test_commit label-upstream conflict-file upstream-content &&
+	test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
+	test_grep "^<<<<<<< UP" conflict-file &&
+	test_grep "^||||||| Stash base" conflict-file &&
+	test_grep "^>>>>>>> STASH" conflict-file
+'
+
+test_expect_success 'apply with empty conflict labels' '
+	git reset --hard initial &&
+	test_commit empty-label-base conflict-file base-content &&
+	echo stashed >conflict-file &&
+	git stash push -m "stashed" &&
+	test_commit empty-label-upstream conflict-file upstream-content &&
+	test_must_fail git stash apply --label-ours= --label-theirs= &&
+	test_grep "^<<<<<<<$" conflict-file &&
+	test_grep "^>>>>>>>$" conflict-file
+'
+
 test_done
diff --git a/xdiff/xmerge.c b/xdiff/xmerge.c
index 29dad98c49..659ad4ec97 100644
--- a/xdiff/xmerge.c
+++ b/xdiff/xmerge.c
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1,
 			      int size, int i, int style,
 			      xdmerge_t *m, char *dest, int marker_size)
 {
-	int marker1_size = (name1 ? strlen(name1) + 1 : 0);
-	int marker2_size = (name2 ? strlen(name2) + 1 : 0);
-	int marker3_size = (name3 ? strlen(name3) + 1 : 0);
+	int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0);
+	int marker2_size = (name2 && *name2 ? strlen(name2) + 1 : 0);
+	int marker3_size = (name3 && *name3 ? strlen(name3) + 1 : 0);
 	int needs_cr = is_cr_needed(xe1, xe2, m);
 
 	if (marker_size <= 0)
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v15 2/5] sequencer: allow create_autostash to run silently
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Add a silent parameter to create_autostash_internal and introduce
create_autostash_ref_silent so that callers can create an autostash
without printing the "Created autostash" message.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/merge.c |  6 ++++--
 sequencer.c     | 17 +++++++++++------
 sequencer.h     |  3 ++-
 3 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/builtin/merge.c b/builtin/merge.c
index 2cbce56f8d..3ebe190ef1 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -1672,7 +1672,8 @@ int cmd_merge(int argc,
 		}
 
 		if (autostash)
-			create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+			create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+					     NULL, false);
 		if (checkout_fast_forward(the_repository,
 					  &head_commit->object.oid,
 					  &commit->object.oid,
@@ -1764,7 +1765,8 @@ int cmd_merge(int argc,
 		die_ff_impossible();
 
 	if (autostash)
-		create_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+		create_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+				     NULL, false);
 
 	/* We are going to make a new commit. */
 	git_committer_info(IDENT_STRICT);
diff --git a/sequencer.c b/sequencer.c
index b7d8dca47f..ff5258f481 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4657,7 +4657,9 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)
 
 static void create_autostash_internal(struct repository *r,
 				      const char *path,
-				      const char *refname)
+				      const char *refname,
+				      const char *message,
+				      bool silent)
 {
 	struct strbuf buf = STRBUF_INIT;
 	struct lock_file lock_file = LOCK_INIT;
@@ -4679,7 +4681,8 @@ static void create_autostash_internal(struct repository *r,
 		struct object_id oid;
 
 		strvec_pushl(&stash.args,
-			     "stash", "create", "autostash", NULL);
+			     "stash", "create",
+			     message ? message : "autostash", NULL);
 		stash.git_cmd = 1;
 		stash.no_stdin = 1;
 		strbuf_reset(&buf);
@@ -4702,7 +4705,8 @@ static void create_autostash_internal(struct repository *r,
 					&oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR);
 		}
 
-		printf(_("Created autostash: %s\n"), buf.buf);
+		if (!silent)
+			printf(_("Created autostash: %s\n"), buf.buf);
 		if (reset_head(r, &ropts) < 0)
 			die(_("could not reset --hard"));
 		discard_index(r->index);
@@ -4714,12 +4718,13 @@ static void create_autostash_internal(struct repository *r,
 
 void create_autostash(struct repository *r, const char *path)
 {
-	create_autostash_internal(r, path, NULL);
+	create_autostash_internal(r, path, NULL, NULL, false);
 }
 
-void create_autostash_ref(struct repository *r, const char *refname)
+void create_autostash_ref(struct repository *r, const char *refname,
+			  const char *message, bool silent)
 {
-	create_autostash_internal(r, NULL, refname);
+	create_autostash_internal(r, NULL, refname, message, silent);
 }
 
 static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
diff --git a/sequencer.h b/sequencer.h
index a6fa670c7c..02d2d9db06 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -229,7 +229,8 @@ void commit_post_rewrite(struct repository *r,
 			 const struct object_id *new_head);
 
 void create_autostash(struct repository *r, const char *path);
-void create_autostash_ref(struct repository *r, const char *refname);
+void create_autostash_ref(struct repository *r, const char *refname,
+			  const char *message, bool silent);
 int save_autostash(const char *path);
 int save_autostash_ref(struct repository *r, const char *refname);
 int apply_autostash(const char *path);
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v15 3/5] sequencer: teach autostash apply to take optional conflict marker labels
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

Add label_ours, label_theirs, label_base, and stash_msg parameters to
apply_autostash_ref() and the autostash apply machinery so callers can
pass custom conflict marker labels through to
"git stash apply --label-ours/--label-theirs/--label-base", as well as
a custom stash message for "git stash store -m".

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/commit.c |  3 ++-
 builtin/merge.c  |  9 ++++++---
 sequencer.c      | 38 +++++++++++++++++++++++++++++---------
 sequencer.h      |  4 +++-
 4 files changed, 40 insertions(+), 14 deletions(-)

diff --git a/builtin/commit.c b/builtin/commit.c
index a3e52ac9ca..28f6174503 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -1979,7 +1979,8 @@ int cmd_commit(int argc,
 				     &oid, flags);
 	}
 
-	apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+	apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+			    NULL, NULL, NULL, NULL);
 
 cleanup:
 	free_commit_extra_headers(extra);
diff --git a/builtin/merge.c b/builtin/merge.c
index 3ebe190ef1..aacf8c524e 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -537,7 +537,8 @@ static void finish(struct commit *head_commit,
 	run_hooks_l(the_repository, "post-merge", squash ? "1" : "0", NULL);
 
 	if (new_head)
-		apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+		apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+				    NULL, NULL, NULL, NULL);
 	strbuf_release(&reflog_message);
 }
 
@@ -1678,7 +1679,8 @@ int cmd_merge(int argc,
 					  &head_commit->object.oid,
 					  &commit->object.oid,
 					  overwrite_ignore)) {
-			apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+			apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+					    NULL, NULL, NULL, NULL);
 			ret = 1;
 			goto done;
 		}
@@ -1851,7 +1853,8 @@ int cmd_merge(int argc,
 		else
 			fprintf(stderr, _("Merge with strategy %s failed.\n"),
 				use_strategies[0]->name);
-		apply_autostash_ref(the_repository, "MERGE_AUTOSTASH");
+		apply_autostash_ref(the_repository, "MERGE_AUTOSTASH",
+				    NULL, NULL, NULL, NULL);
 		ret = 2;
 		goto done;
 	} else if (best_strategy == wt_strategy)
diff --git a/sequencer.c b/sequencer.c
index ff5258f481..7c0376d9e4 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4727,7 +4727,10 @@ void create_autostash_ref(struct repository *r, const char *refname,
 	create_autostash_internal(r, NULL, refname, message, silent);
 }
 
-static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
+static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
+				    const char *label_ours, const char *label_theirs,
+				    const char *label_base,
+				    const char *stash_msg)
 {
 	struct child_process child = CHILD_PROCESS_INIT;
 	int ret = 0;
@@ -4738,6 +4741,12 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
 		child.no_stderr = 1;
 		strvec_push(&child.args, "stash");
 		strvec_push(&child.args, "apply");
+		if (label_ours)
+			strvec_pushf(&child.args, "--label-ours=%s", label_ours);
+		if (label_theirs)
+			strvec_pushf(&child.args, "--label-theirs=%s", label_theirs);
+		if (label_base)
+			strvec_pushf(&child.args, "--label-base=%s", label_base);
 		strvec_push(&child.args, stash_oid);
 		ret = run_command(&child);
 	}
@@ -4751,7 +4760,7 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
 		strvec_push(&store.args, "stash");
 		strvec_push(&store.args, "store");
 		strvec_push(&store.args, "-m");
-		strvec_push(&store.args, "autostash");
+		strvec_push(&store.args, stash_msg ? stash_msg : "autostash");
 		strvec_push(&store.args, "-q");
 		strvec_push(&store.args, stash_oid);
 		if (run_command(&store))
@@ -4782,7 +4791,8 @@ static int apply_save_autostash(const char *path, int attempt_apply)
 	}
 	strbuf_trim(&stash_oid);
 
-	ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply);
+	ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,
+				      NULL, NULL, NULL, NULL);
 
 	unlink(path);
 	strbuf_release(&stash_oid);
@@ -4801,11 +4811,14 @@ int apply_autostash(const char *path)
 
 int apply_autostash_oid(const char *stash_oid)
 {
-	return apply_save_autostash_oid(stash_oid, 1);
+	return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL);
 }
 
 static int apply_save_autostash_ref(struct repository *r, const char *refname,
-				    int attempt_apply)
+				    int attempt_apply,
+				    const char *label_ours, const char *label_theirs,
+				    const char *label_base,
+				    const char *stash_msg)
 {
 	struct object_id stash_oid;
 	char stash_oid_hex[GIT_MAX_HEXSZ + 1];
@@ -4821,7 +4834,9 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
 		return error(_("autostash reference is a symref"));
 
 	oid_to_hex_r(stash_oid_hex, &stash_oid);
-	ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply);
+	ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,
+				       label_ours, label_theirs, label_base,
+				       stash_msg);
 
 	refs_delete_ref(get_main_ref_store(r), "", refname,
 			&stash_oid, REF_NO_DEREF);
@@ -4831,12 +4846,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
 
 int save_autostash_ref(struct repository *r, const char *refname)
 {
-	return apply_save_autostash_ref(r, refname, 0);
+	return apply_save_autostash_ref(r, refname, 0,
+					NULL, NULL, NULL, NULL);
 }
 
-int apply_autostash_ref(struct repository *r, const char *refname)
+int apply_autostash_ref(struct repository *r, const char *refname,
+			const char *label_ours, const char *label_theirs,
+			const char *label_base, const char *stash_msg)
 {
-	return apply_save_autostash_ref(r, refname, 1);
+	return apply_save_autostash_ref(r, refname, 1,
+					label_ours, label_theirs, label_base,
+					stash_msg);
 }
 
 static int checkout_onto(struct repository *r, struct replay_opts *opts,
diff --git a/sequencer.h b/sequencer.h
index 02d2d9db06..3164bd437d 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -235,7 +235,9 @@ int save_autostash(const char *path);
 int save_autostash_ref(struct repository *r, const char *refname);
 int apply_autostash(const char *path);
 int apply_autostash_oid(const char *stash_oid);
-int apply_autostash_ref(struct repository *r, const char *refname);
+int apply_autostash_ref(struct repository *r, const char *refname,
+			const char *label_ours, const char *label_theirs,
+			const char *label_base, const char *stash_msg);
 
 #define SUMMARY_INITIAL_COMMIT   (1 << 0)
 #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1)
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v15 4/5] checkout: rollback lock on early returns in merge_working_tree
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

merge_working_tree() acquires the index lock via
repo_hold_locked_index() but several early return paths exit
without calling rollback_lock_file(), leaving the lock held.
While this is currently harmless because the process exits soon
after, it becomes a problem if the function is ever called more
than once in the same process.

Add rollback_lock_file() calls to all early return paths.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/checkout.c | 29 ++++++++++++++++++++++-------
 1 file changed, 22 insertions(+), 7 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index e031e61886..c80c62b37b 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -783,8 +783,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
 	struct tree *new_tree;
 
 	repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
-	if (repo_read_index_preload(the_repository, NULL, 0) < 0)
+	if (repo_read_index_preload(the_repository, NULL, 0) < 0) {
+		rollback_lock_file(&lock_file);
 		return error(_("index file corrupt"));
+	}
 
 	resolve_undo_clear_index(the_repository->index);
 	if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
@@ -797,14 +799,18 @@ static int merge_working_tree(const struct checkout_opts *opts,
 	} else {
 		new_tree = repo_get_commit_tree(the_repository,
 						new_branch_info->commit);
-		if (!new_tree)
+		if (!new_tree) {
+			rollback_lock_file(&lock_file);
 			return error(_("unable to read tree (%s)"),
 				     oid_to_hex(&new_branch_info->commit->object.oid));
+		}
 	}
 	if (opts->discard_changes) {
 		ret = reset_tree(new_tree, opts, 1, writeout_error, new_branch_info);
-		if (ret)
+		if (ret) {
+			rollback_lock_file(&lock_file);
 			return ret;
+		}
 	} else {
 		struct tree_desc trees[2];
 		struct tree *tree;
@@ -814,6 +820,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
 		refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
 
 		if (unmerged_index(the_repository->index)) {
+			rollback_lock_file(&lock_file);
 			error(_("you need to resolve your current index first"));
 			return 1;
 		}
@@ -857,15 +864,19 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			struct strbuf sb = STRBUF_INIT;
 			struct strbuf old_commit_shortname = STRBUF_INIT;
 
-			if (!opts->merge)
+			if (!opts->merge) {
+				rollback_lock_file(&lock_file);
 				return 1;
+			}
 
 			/*
 			 * Without old_branch_info->commit, the below is the same as
 			 * the two-tree unpack we already tried and failed.
 			 */
-			if (!old_branch_info->commit)
+			if (!old_branch_info->commit) {
+				rollback_lock_file(&lock_file);
 				return 1;
+			}
 			old_tree = repo_get_commit_tree(the_repository,
 							old_branch_info->commit);
 
@@ -897,8 +908,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			ret = reset_tree(new_tree,
 					 opts, 1,
 					 writeout_error, new_branch_info);
-			if (ret)
+			if (ret) {
+				rollback_lock_file(&lock_file);
 				return ret;
+			}
 			o.ancestor = old_branch_info->name;
 			if (!old_branch_info->name) {
 				strbuf_add_unique_abbrev(&old_commit_shortname,
@@ -920,8 +933,10 @@ static int merge_working_tree(const struct checkout_opts *opts,
 					 writeout_error, new_branch_info);
 			strbuf_release(&o.obuf);
 			strbuf_release(&old_commit_shortname);
-			if (ret)
+			if (ret) {
+				rollback_lock_file(&lock_file);
 				return ret;
+			}
 		}
 	}
 
-- 
gitgitgadget


^ permalink raw reply related

* [PATCH v15 5/5] checkout -m: autostash when switching branches
From: Harald Nordgren via GitGitGadget @ 2026-04-24 21:10 UTC (permalink / raw)
  To: git; +Cc: Phillip Wood, Chris Torek, Jeff King, Harald Nordgren,
	Harald Nordgren
In-Reply-To: <pull.2234.v15.git.git.1777065012.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

When switching branches with "git checkout -m", the attempted merge
of local modifications may cause conflicts with the changes made on
the other branch, which the user may not want to (or may not be able
to) resolve right now.  Because there is no easy way to recover from
this situation, we discouraged users from using "checkout -m" unless
they are certain their changes are trivial and within their ability
to resolve conflicts.

Teach the -m flow to create a temporary stash before switching and
reapply it after.  On success, the stash is silently applied and
the list of locally modified paths is shown, same as a successful
"git checkout" without "-m".

If reapplying causes conflicts, the stash is kept and the user is
told they can resolve and run "git stash drop", or run "git reset
--hard" and later "git stash pop" to recover their changes.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 Documentation/git-checkout.adoc |  58 ++++++------
 Documentation/git-switch.adoc   |  33 +++----
 builtin/checkout.c              | 160 ++++++++++++++------------------
 sequencer.c                     |  14 ++-
 t/t3420-rebase-autostash.sh     |  16 ++--
 t/t7201-co.sh                   |  61 +++++++++++-
 t/t7600-merge.sh                |   3 +-
 xdiff-interface.c               |  12 +++
 xdiff-interface.h               |   1 +
 9 files changed, 211 insertions(+), 147 deletions(-)

diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
index 43ccf47cf6..70dd211ee3 100644
--- a/Documentation/git-checkout.adoc
+++ b/Documentation/git-checkout.adoc
@@ -251,20 +251,19 @@ working tree, by copying them from elsewhere, extracting a tarball, etc.
 	are different between the current branch and the branch to
 	which you are switching, the command refuses to switch
 	branches in order to preserve your modifications in context.
-	However, with this option, a three-way merge between the current
-	branch, your working tree contents, and the new branch
-	is done, and you will be on the new branch.
-+
-When a merge conflict happens, the index entries for conflicting
-paths are left unmerged, and you need to resolve the conflicts
-and mark the resolved paths with `git add` (or `git rm` if the merge
-should result in deletion of the path).
+	With this option, the conflicting local changes are
+	automatically stashed before the switch and reapplied
+	afterwards.  If the local changes do not overlap with the
+	differences between branches, the switch proceeds without
+	stashing.  If reapplying the stash results in conflicts, the
+	entry is saved to the stash list.  Resolve the conflicts
+	and run `git stash drop` when done, or clear the working
+	tree (e.g. with `git reset --hard`) before running `git stash
+	pop` later to re-apply your changes.
 +
 When checking out paths from the index, this option lets you recreate
 the conflicted merge in the specified paths.  This option cannot be
 used when checking out paths from a tree-ish.
-+
-When switching branches with `--merge`, staged changes may be lost.
 
 `--conflict=<style>`::
 	The same as `--merge` option above, but changes the way the
@@ -578,39 +577,44 @@ $ git checkout mytopic
 error: You have local changes to 'frotz'; not switching branches.
 ------------
 
-You can give the `-m` flag to the command, which would try a
-three-way merge:
+You can give the `-m` flag to the command, which would carry your local
+changes to the new branch:
 
 ------------
 $ git checkout -m mytopic
-Auto-merging frotz
+Switched to branch 'mytopic'
 ------------
 
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
 registered in your index file, so `git diff` would show you what
 changes you made since the tip of the new branch.
 
 === 3. Merge conflict
 
-When a merge conflict happens during switching branches with
-the `-m` option, you would see something like this:
+When the `--merge` (`-m`) option is in effect and the locally
+modified files overlap with files that need to be updated by the
+branch switch, the changes are stashed and reapplied after the
+switch.  If this process results in conflicts, a stash entry is saved
+and made available in `git stash list`:
 
 ------------
 $ git checkout -m mytopic
-Auto-merging frotz
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
+Your local changes are stashed, however, applying it to carry
+forward your local changes resulted in conflicts:
 
-At this point, `git diff` shows the changes cleanly merged as in
-the previous example, as well as the changes in the conflicted
-files.  Edit and resolve the conflict and mark it resolved with
-`git add` as usual:
+ - You can try resolving them now.  If you resolved them
+   successfully, discard the stash entry with "git stash drop".
 
+ - Alternatively you can "git reset --hard" if you do not want
+   to deal with them right now, and later "git stash pop" to
+   recover your local changes.
 ------------
-$ edit frotz
-$ git add frotz
-------------
+
+You can try resolving the conflicts now.  Edit the conflicting files
+and mark them resolved with `git add` as usual, then run `git stash
+drop` to discard the stash entry.  Alternatively, you can clear the
+working tree with `git reset --hard` and recover your local changes
+later with `git stash pop`.
 
 CONFIGURATION
 -------------
diff --git a/Documentation/git-switch.adoc b/Documentation/git-switch.adoc
index 87707e9265..ee58a4d0fd 100644
--- a/Documentation/git-switch.adoc
+++ b/Documentation/git-switch.adoc
@@ -123,18 +123,19 @@ variable.
 
 `-m`::
 `--merge`::
-	If you have local modifications to one or more files that are
-	different between the current branch and the branch to which
-	you are switching, the command refuses to switch branches in
-	order to preserve your modifications in context.  However,
-	with this option, a three-way merge between the current
-	branch, your working tree contents, and the new branch is
-	done, and you will be on the new branch.
-+
-When a merge conflict happens, the index entries for conflicting
-paths are left unmerged, and you need to resolve the conflicts
-and mark the resolved paths with `git add` (or `git rm` if the merge
-should result in deletion of the path).
+	If you have local modifications to one or more files that
+	are different between the current branch and the branch to
+	which you are switching, the command normally refuses to
+	switch branches in order to preserve your modifications in
+	context.  However, with this option, the conflicting local
+	changes are automatically stashed before the switch and
+	reapplied afterwards.  If the local changes do not overlap
+	with the differences between branches, the switch proceeds
+	without stashing.  If reapplying the stash results in
+	conflicts, the entry is saved to the stash list.  Resolve
+	the conflicts and run `git stash drop` when done, or clear
+	the working tree (e.g. with `git reset --hard`) before
+	running `git stash pop` later to re-apply your changes.
 
 `--conflict=<style>`::
 	The same as `--merge` option above, but changes the way the
@@ -217,15 +218,15 @@ $ git switch mytopic
 error: You have local changes to 'frotz'; not switching branches.
 ------------
 
-You can give the `-m` flag to the command, which would try a three-way
-merge:
+You can give the `-m` flag to the command, which would carry your local
+changes to the new branch:
 
 ------------
 $ git switch -m mytopic
-Auto-merging frotz
+Switched to branch 'mytopic'
 ------------
 
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
 registered in your index file, so `git diff` would show you what
 changes you made since the tip of the new branch.
 
diff --git a/builtin/checkout.c b/builtin/checkout.c
index c80c62b37b..d02183b245 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -17,7 +17,6 @@
 #include "merge-ll.h"
 #include "lockfile.h"
 #include "mem-pool.h"
-#include "merge-ort-wrappers.h"
 #include "object-file.h"
 #include "object-name.h"
 #include "odb.h"
@@ -30,6 +29,7 @@
 #include "repo-settings.h"
 #include "resolve-undo.h"
 #include "revision.h"
+#include "sequencer.h"
 #include "setup.h"
 #include "submodule.h"
 #include "symlinks.h"
@@ -99,6 +99,8 @@ struct checkout_opts {
 	.auto_advance = 1, \
 }
 
+#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
+
 struct branch_info {
 	char *name; /* The short name used */
 	char *path; /* The full name of a real branch */
@@ -755,7 +757,7 @@ static void setup_branch_path(struct branch_info *branch)
 
 static void init_topts(struct unpack_trees_options *topts, int merge,
 		       int show_progress, int overwrite_ignore,
-		       struct commit *old_commit)
+		       struct commit *old_commit, bool show_unpack_errors)
 {
 	memset(topts, 0, sizeof(*topts));
 	topts->head_idx = -1;
@@ -767,7 +769,7 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
 	topts->initial_checkout = is_index_unborn(the_repository->index);
 	topts->update = 1;
 	topts->merge = 1;
-	topts->quiet = merge && old_commit;
+	topts->quiet = merge && old_commit && !show_unpack_errors;
 	topts->verbose_update = show_progress;
 	topts->fn = twoway_merge;
 	topts->preserve_ignored = !overwrite_ignore;
@@ -776,7 +778,8 @@ static void init_topts(struct unpack_trees_options *topts, int merge,
 static int merge_working_tree(const struct checkout_opts *opts,
 			      struct branch_info *old_branch_info,
 			      struct branch_info *new_branch_info,
-			      int *writeout_error)
+			      int *writeout_error,
+			      bool show_unpack_errors)
 {
 	int ret;
 	struct lock_file lock_file = LOCK_INIT;
@@ -827,7 +830,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
 
 		/* 2-way merge to the new branch */
 		init_topts(&topts, opts->merge, opts->show_progress,
-			   opts->overwrite_ignore, old_branch_info->commit);
+			   opts->overwrite_ignore, old_branch_info->commit,
+			   show_unpack_errors);
 		init_checkout_metadata(&topts.meta, new_branch_info->refname,
 				       new_branch_info->commit ?
 				       &new_branch_info->commit->object.oid :
@@ -853,90 +857,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
 		ret = unpack_trees(2, trees, &topts);
 		clear_unpack_trees_porcelain(&topts);
 		if (ret == -1) {
-			/*
-			 * Unpack couldn't do a trivial merge; either
-			 * give up or do a real merge, depending on
-			 * whether the merge flag was used.
-			 */
-			struct tree *work;
-			struct tree *old_tree;
-			struct merge_options o;
-			struct strbuf sb = STRBUF_INIT;
-			struct strbuf old_commit_shortname = STRBUF_INIT;
-
-			if (!opts->merge) {
-				rollback_lock_file(&lock_file);
-				return 1;
-			}
-
-			/*
-			 * Without old_branch_info->commit, the below is the same as
-			 * the two-tree unpack we already tried and failed.
-			 */
-			if (!old_branch_info->commit) {
-				rollback_lock_file(&lock_file);
-				return 1;
-			}
-			old_tree = repo_get_commit_tree(the_repository,
-							old_branch_info->commit);
-
-			if (repo_index_has_changes(the_repository, old_tree, &sb))
-				die(_("cannot continue with staged changes in "
-				      "the following files:\n%s"), sb.buf);
-			strbuf_release(&sb);
-
-			/* Do more real merge */
-
-			/*
-			 * We update the index fully, then write the
-			 * tree from the index, then merge the new
-			 * branch with the current tree, with the old
-			 * branch as the base. Then we reset the index
-			 * (but not the working tree) to the new
-			 * branch, leaving the working tree as the
-			 * merged version, but skipping unmerged
-			 * entries in the index.
-			 */
-
-			add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
-					0, 0);
-			init_ui_merge_options(&o, the_repository);
-			o.verbosity = 0;
-			work = write_in_core_index_as_tree(the_repository,
-							   the_repository->index);
-
-			ret = reset_tree(new_tree,
-					 opts, 1,
-					 writeout_error, new_branch_info);
-			if (ret) {
-				rollback_lock_file(&lock_file);
-				return ret;
-			}
-			o.ancestor = old_branch_info->name;
-			if (!old_branch_info->name) {
-				strbuf_add_unique_abbrev(&old_commit_shortname,
-							 &old_branch_info->commit->object.oid,
-							 DEFAULT_ABBREV);
-				o.ancestor = old_commit_shortname.buf;
-			}
-			o.branch1 = new_branch_info->name;
-			o.branch2 = "local";
-			o.conflict_style = opts->conflict_style;
-			ret = merge_ort_nonrecursive(&o,
-						     new_tree,
-						     work,
-						     old_tree);
-			if (ret < 0)
-				die(NULL);
-			ret = reset_tree(new_tree,
-					 opts, 0,
-					 writeout_error, new_branch_info);
-			strbuf_release(&o.obuf);
-			strbuf_release(&old_commit_shortname);
-			if (ret) {
-				rollback_lock_file(&lock_file);
-				return ret;
-			}
+			rollback_lock_file(&lock_file);
+			return MERGE_WORKING_TREE_UNPACK_FAILED;
 		}
 	}
 
@@ -1181,6 +1103,10 @@ static int switch_branches(const struct checkout_opts *opts,
 	struct object_id rev;
 	int flag, writeout_error = 0;
 	int do_merge = 1;
+	int created_autostash = 0;
+	struct strbuf old_commit_shortname = STRBUF_INIT;
+	struct strbuf autostash_msg = STRBUF_INIT;
+	const char *stash_label_base = NULL;
 
 	trace2_cmd_mode("branch");
 
@@ -1218,11 +1144,49 @@ static int switch_branches(const struct checkout_opts *opts,
 			do_merge = 0;
 	}
 
+	if (old_branch_info.name) {
+		stash_label_base = old_branch_info.name;
+	} else if (old_branch_info.commit) {
+		strbuf_add_unique_abbrev(&old_commit_shortname,
+					 &old_branch_info.commit->object.oid,
+					 DEFAULT_ABBREV);
+		stash_label_base = old_commit_shortname.buf;
+	}
+
 	if (do_merge) {
-		ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+		ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+					 &writeout_error, false);
+		if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+			strbuf_addf(&autostash_msg,
+				    "autostash while switching to '%s'",
+				    new_branch_info->name);
+			create_autostash_ref(the_repository,
+					     "CHECKOUT_AUTOSTASH_HEAD",
+					     autostash_msg.buf, true);
+			created_autostash = 1;
+			ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+						 &writeout_error, true);
+		}
+		if (created_autostash) {
+			if (opts->conflict_style >= 0) {
+				struct strbuf cfg = STRBUF_INIT;
+				strbuf_addf(&cfg, "merge.conflictStyle=%s",
+					    conflict_style_name(opts->conflict_style));
+				git_config_push_parameter(cfg.buf);
+				strbuf_release(&cfg);
+			}
+			apply_autostash_ref(the_repository,
+					    "CHECKOUT_AUTOSTASH_HEAD",
+					    new_branch_info->name,
+					    "local",
+					    stash_label_base,
+					    autostash_msg.buf);
+		}
 		if (ret) {
 			branch_info_release(&old_branch_info);
-			return ret;
+			strbuf_release(&old_commit_shortname);
+			strbuf_release(&autostash_msg);
+			return ret < 0 ? 1 : ret;
 		}
 	}
 
@@ -1231,8 +1195,22 @@ static int switch_branches(const struct checkout_opts *opts,
 
 	update_refs_for_switch(opts, &old_branch_info, new_branch_info);
 
+	if (created_autostash) {
+		discard_index(the_repository->index);
+		if (repo_read_index(the_repository) < 0)
+			die(_("index file corrupt"));
+
+		if (!opts->quiet && new_branch_info->commit) {
+			printf(_("The following paths have local changes:\n"));
+			show_local_changes(&new_branch_info->commit->object,
+					   &opts->diff_options);
+		}
+	}
+
 	ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
 	branch_info_release(&old_branch_info);
+	strbuf_release(&old_commit_shortname);
+	strbuf_release(&autostash_msg);
 
 	return ret || writeout_error;
 }
diff --git a/sequencer.c b/sequencer.c
index 7c0376d9e4..746f85a442 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -4765,15 +4765,19 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply,
 		strvec_push(&store.args, stash_oid);
 		if (run_command(&store))
 			ret = error(_("cannot store %s"), stash_oid);
+		else if (attempt_apply)
+			fprintf(stderr,
+				_("Your local changes are stashed, however applying them\n"
+				  "resulted in conflicts.  You can either resolve the conflicts\n"
+				  "and then discard the stash with \"git stash drop\", or, if you\n"
+				  "do not want to resolve them now, run \"git reset --hard\" and\n"
+				  "apply the local changes later by running \"git stash pop\".\n"));
 		else
 			fprintf(stderr,
-				_("%s\n"
+				_("Autostash exists; creating a new stash entry.\n"
 				  "Your changes are safe in the stash.\n"
 				  "You can run \"git stash pop\" or"
-				  " \"git stash drop\" at any time.\n"),
-				attempt_apply ?
-				_("Applying autostash resulted in conflicts.") :
-				_("Autostash exists; creating a new stash entry."));
+				  " \"git stash drop\" at any time.\n"));
 	}
 
 	return ret;
diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh
index ad3ba6a984..f0bbc476ff 100755
--- a/t/t3420-rebase-autostash.sh
+++ b/t/t3420-rebase-autostash.sh
@@ -61,18 +61,22 @@ create_expected_failure_apply () {
 	First, rewinding head to replay your work on top of it...
 	Applying: second commit
 	Applying: third commit
-	Applying autostash resulted in conflicts.
-	Your changes are safe in the stash.
-	You can run "git stash pop" or "git stash drop" at any time.
+	Your local changes are stashed, however applying them
+	resulted in conflicts.  You can either resolve the conflicts
+	and then discard the stash with "git stash drop", or, if you
+	do not want to resolve them now, run "git reset --hard" and
+	apply the local changes later by running "git stash pop".
 	EOF
 }
 
 create_expected_failure_merge () {
 	cat >expected <<-EOF
 	$(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
-	Applying autostash resulted in conflicts.
-	Your changes are safe in the stash.
-	You can run "git stash pop" or "git stash drop" at any time.
+	Your local changes are stashed, however applying them
+	resulted in conflicts.  You can either resolve the conflicts
+	and then discard the stash with "git stash drop", or, if you
+	do not want to resolve them now, run "git reset --hard" and
+	apply the local changes later by running "git stash pop".
 	Successfully rebased and updated refs/heads/rebased-feature-branch.
 	EOF
 }
diff --git a/t/t7201-co.sh b/t/t7201-co.sh
index 9bcf7c0b40..b3293ead8d 100755
--- a/t/t7201-co.sh
+++ b/t/t7201-co.sh
@@ -102,7 +102,7 @@ test_expect_success 'checkout -m with dirty tree' '
 
 	test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
 
-	printf "M\t%s\n" one >expect.messages &&
+	printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
 	test_cmp expect.messages messages &&
 
 	fill "M	one" "A	three" "D	two" >expect.main &&
@@ -210,6 +210,65 @@ test_expect_success 'checkout --merge --conflict=diff3 <branch>' '
 	test_cmp expect two
 '
 
+test_expect_success 'checkout -m with mixed staged and unstaged changes' '
+	git checkout -f main &&
+	git clean -f &&
+
+	fill 0 x y z >same &&
+	git add same &&
+	fill 1 2 3 4 5 6 7 >one &&
+	git checkout -m side >actual 2>&1 &&
+	test_grep "Applied autostash" actual &&
+	fill 0 x y z >expect &&
+	test_cmp expect same &&
+	fill 1 2 3 4 5 6 7 >expect &&
+	test_cmp expect one
+'
+
+test_expect_success 'checkout -m creates a recoverable stash on conflict' '
+	git checkout -f main &&
+	git clean -f &&
+
+	fill 1 2 3 4 5 >one &&
+	test_must_fail git checkout side 2>stderr &&
+	test_grep "Your local changes" stderr &&
+	git checkout -m side >actual 2>&1 &&
+	test_grep "resulted in conflicts" actual &&
+	test_grep "git stash drop" actual &&
+	test_grep "git stash pop" actual &&
+	test_grep "The following paths have local changes" actual &&
+	git show --format=%B --diff-merges=1 refs/stash >actual &&
+	sed /^index/d actual >actual.trimmed &&
+	cat >expect <<-EOF &&
+	On main: autostash while switching to ${SQ}side${SQ}
+
+	diff --git a/one b/one
+	--- a/one
+	+++ b/one
+	@@ -3,6 +3,3 @@
+	 3
+	 4
+	 5
+	-6
+	-7
+	-8
+	EOF
+	test_cmp expect actual.trimmed &&
+	git stash drop &&
+	git reset --hard
+'
+
+test_expect_success 'checkout -m which would overwrite untracked file' '
+	git checkout -f --detach main &&
+	test_commit another-file &&
+	git checkout HEAD^ &&
+	>another-file.t &&
+	fill 1 2 3 4 5 >one &&
+	test_must_fail git checkout -m @{-1} 2>err &&
+	test_grep "would be overwritten by checkout" err &&
+	test_grep "another-file.t" err
+'
+
 test_expect_success 'switch to another branch while carrying a deletion' '
 	git checkout -f main &&
 	git reset --hard &&
diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh
index 9838094b66..f877d9a433 100755
--- a/t/t7600-merge.sh
+++ b/t/t7600-merge.sh
@@ -914,7 +914,8 @@ test_expect_success 'merge with conflicted --autostash changes' '
 	git diff >expect &&
 	test_when_finished "test_might_fail git stash drop" &&
 	git merge --autostash c3 2>err &&
-	test_grep "Applying autostash resulted in conflicts." err &&
+	test_grep "applying them" err &&
+	test_grep "resulted in conflicts" err &&
 	git show HEAD:file >merge-result &&
 	test_cmp result.1-9 merge-result &&
 	git stash show -p >actual &&
diff --git a/xdiff-interface.c b/xdiff-interface.c
index f043330f2a..5ee2b96d0a 100644
--- a/xdiff-interface.c
+++ b/xdiff-interface.c
@@ -325,6 +325,18 @@ int parse_conflict_style_name(const char *value)
 		return -1;
 }
 
+const char *conflict_style_name(int style)
+{
+	switch (style) {
+	case XDL_MERGE_DIFF3:
+		return "diff3";
+	case XDL_MERGE_ZEALOUS_DIFF3:
+		return "zdiff3";
+	default:
+		return "merge";
+	}
+}
+
 int git_xmerge_style = -1;
 
 int git_xmerge_config(const char *var, const char *value,
diff --git a/xdiff-interface.h b/xdiff-interface.h
index fbc4ceec40..ce54e1c0e0 100644
--- a/xdiff-interface.h
+++ b/xdiff-interface.h
@@ -55,6 +55,7 @@ void xdiff_set_find_func(xdemitconf_t *xecfg, const char *line, int cflags);
 void xdiff_clear_find_func(xdemitconf_t *xecfg);
 struct config_context;
 int parse_conflict_style_name(const char *value);
+const char *conflict_style_name(int style);
 int git_xmerge_config(const char *var, const char *value,
 		      const struct config_context *ctx, void *cb);
 extern int git_xmerge_style;
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Junio C Hamano @ 2026-04-24 22:21 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren
In-Reply-To: <pull.2281.git.git.1777024991531.gitgitgadget@gmail.com>

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> Add a --fetch option to git checkout and git switch, plus a
> checkout.autoFetch config to enable it by default. When set and the
> start-point argument names a configured remote (either bare, like
> "origin", or prefixed, like "origin/foo"), fetch that remote before
> resolving the ref. Aborts the checkout if the fetch fails.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---

It is true that "checkout" does funny things to special case the
remote-tracking branches, like setting up the branch.<name>.merge
configuration or even inferring the name of the local branch to be
created.  

But I have to say that this one, especially the configuration
variable, goes way too far.  The usual uses of remote-tracking
branch names, e.g.,

    git log -1 origin/master
    git grep frotz origin/master
    git rev-list --count origin/maint..origin/master

to name a specific object all assume and rely on the stability of
them.  Should the configuration cause a fetch to happen before any
of these uses of remote-tracking branches for consistency?

^ permalink raw reply

* Re: [PATCH] alias: restore support for simple dotted aliases
From: Junio C Hamano @ 2026-04-24 22:47 UTC (permalink / raw)
  To: Jonatan Holmgren; +Cc: git, peff, rsch, michael.grossfeld
In-Reply-To: <20260424151053.917066-1-jonatan@jontes.page>

Jonatan Holmgren <jonatan@jontes.page> writes:

> Historically, config entries like alias.foo.bar expanded the alias
> "foo.bar". The subsection-based alias syntax introduced in
> ac1f12a9de (alias: support non-alphanumeric names via subsection
> syntax, 2026-02-18) broke that behavior by treating such entries as
> if they were subsection syntax.
>
> Restore support for the old dotted form by falling back to the full
> name when the final key is not "command". Add tests covering execution
> and help output for simple dotted aliases.
>
> Reported-by: Michael Grossfeld <michael.grossfeld@amd.com>
> Helped-by: Jeff King <peff@peff.net>
> ---
>  alias.c          | 16 ++++++++++++++--
>  help.c           |  9 ++++++++-
>  t/t0014-alias.sh | 12 ++++++++++++
>  3 files changed, 34 insertions(+), 3 deletions(-)

Do we lose the extensibility introduced by the new syntax by going
this route, though?  I would imagine that

    [alias "frotz"]
	command = !"nitfol"
	help = "run nitfol command"

would have been a natural first addition to the current system to
give help text to the alias, but this change makes such an
extensibility impossible, doesn't it?

If this change robs the extensibility, it makes mse wonder if the
three-level "alias" was a mistake, and we should have instead
introduced a new "nalias" that is three level from the get go.

^ permalink raw reply

* Re: [PATCH] checkout: add --fetch to fetch remote before resolving start-point
From: Junio C Hamano @ 2026-04-25  2:54 UTC (permalink / raw)
  To: Harald Nordgren via GitGitGadget; +Cc: git, Harald Nordgren
In-Reply-To: <xmqqeck4xan3.fsf@gitster.g>

Junio C Hamano <gitster@pobox.com> writes:

> "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> From: Harald Nordgren <haraldnordgren@gmail.com>
>>
>> Add a --fetch option to git checkout and git switch, plus a
>> checkout.autoFetch config to enable it by default. When set and the
>> start-point argument names a configured remote (either bare, like
>> "origin", or prefixed, like "origin/foo"), fetch that remote before
>> resolving the ref. Aborts the checkout if the fetch fails.
>>
>> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
>> ---
>
> It is true that "checkout" does funny things to special case the
> remote-tracking branches, like setting up the branch.<name>.merge
> configuration or even inferring the name of the local branch to be
> created.  
> ...
> ...  Should the configuration cause a fetch to happen before any
> of these uses of remote-tracking branches for consistency?

The last one was a rhetorical question.  I do not want to see such a
configuration variable to implicitly trigger fetching at all.

I am somewhat sympathetic to the desire "I want to be sure that I
start the new branch in a state as fresh as possible".  It is tied
to the "--track" option of "git checkout -b topic --track
origin/main".  If you are merely starting at a single arbitrary
commit, instead of anticipating to having to repeatedly sync with
the remote-tracking branch that will subsequently move, there is no
point jumping to a "freshest" commit that you haven't even seen let
alone inspected (i.e., you do not even know if it is a good base to
build on).

So instead of introducing a totally new option that can only be used
only when "--track" is given, it might make more sense to introduce
this as a variant of "--track", perhaps "--track=fetch,[in]direct"
or something like that.  And extend branch.autosetup{Merge,Rebase}
that controls what happens when a branch is created with "checkout
-t -b" or "branch --track" so that the remote-tracking branch gets
updated, perhaps.

As to "git checkout origin/main" (nothing else on the command line),
it has "magic" compared to "git checkout origin/main~0" already by
treating the parameter not just as a SHA-1 expression that names a
commit object but as a remote-tracking branch (this is necessary for
"-t").  So I am not fundamentally opposed to the idea to give an
option to treat that form specifically.

Having said all that, quite honestly, I prefer not to see any of the
above changes, including the original patch.  It leaves too many
usability questions unaddressed.  For a starter, if you interact
with a repository with two or more branches, should

   $ git checkout --track=fetch -b topic origin/main

update an unrelated remote-tracking branch origin/maint from the
same remote?  As I already said, most Git tools _depend_ on the
stability of remote-tracking branches---the desire to update the
origin/main when a new branch that builds on origin/main is created
may be a valid one, but it is unclear if that warrants updating
other remote-tracking branches only because they come from the same
remote repository.  There may be a dozen other UI/usability issues
that will be introduced if we start to "fetch from remote"
automatically, but I won't even try to be exhaustive while I am
still on a leave ;-)


^ permalink raw reply

* Re: [PATCH] dir: use per-worktree repository ignore patterns upon request
From: Junio C Hamano @ 2026-04-25  3:06 UTC (permalink / raw)
  To: Phillip Wood
  Cc: D. Ben Knoble, git, brian m . carlson, Patrick Steinhardt,
	Taylor Blau, Caleb White, Calvin Wan, Elijah Newren, Andrew Berry,
	Jeff King, Derrick Stolee
In-Reply-To: <4a8d1289-2e2b-4fd6-8ada-143992dd7c4d@gmail.com>

Phillip Wood <phillip.wood123@gmail.com> writes:

> Sharing the same set of exclude patterns between all worktrees seems 
> like a reasonable thing to do - it would be a pain to have to populate 
> them every time a new worktree was created.
>
> 	git rev-parse --git-path info/exclude
>
> always returns the correct path because it uses repo_git_path() which 
> knows whether a particular path is per-worktree or resides under 
> $GIT_COMMON_DIR. It is best to avoid constructing paths manually using 
> "git rev-parse --git-dir"

Great to see the best practice raised, like this.

>> Some users may prefer each worktree use its own ignore file; some may
>> prefer both; some may prefer the current behavior.
>
> This sounds quite hypothetical - do we have a concrete use case for 
> per-worktree exclude files?

I can see if somebody wants to have different subset of paths
checked out by setting up separate sparse settings.  But marking
what paths are never be part of the project (which is what exclude
is about)?  I do not think it is useful in general.

The only case I can think of is a repository that houses more than
one completely unrelated histories, perhaps being cheapster on a
hosting site that charges per number of repositories or something
silly like that.

My https://git.kernel.org/pub/scm/git/git.git/ repository houses two
completely unrelated histories, one the project data, and 'todo'
that are mostly about tools I personally use to maintain the
project.  Ideally, the latter branch should have been created and
maintained as a separate repository, but back them I didn't have
write access outside pub/scm/git/git.git/ (notably pub/scm/git/
itself was not writable to me) and that was the only reason why that
unrelated history is pushed into the same repository as a separate
and unrelated 'todo' branch.  It is not a recommended practice, and
if this configuration is primarily to cater to such layout, I do not
think we want to add it.

^ permalink raw reply

* Re: [PATCH] alias: restore support for simple dotted aliases
From: Jonatan Holmgren @ 2026-04-25  9:57 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, peff, rsch, michael.grossfeld
In-Reply-To: <xmqqpl3ovuvq.fsf@gitster.g>

That is a challenge we are going to have to consider. I think reserving
`command` is a worthwhile compromise, but obviously we cannot do that for
arbitrary future keys such as `help`, `hidden`, etc.

One possible compromise would be to reserve `command` and `alias-*`, as
neither seems very likely to exist in users' historical alias names.

A new namespace makes the most sense from a namespace-pollution point of
view, but I struggle to see that as good UX. Even a separate namespace
only for alias metadata would make more sense to me than moving aliases
entirely, since subsection aliases with just `command` will likely be 
far more common than any future metadata keys, but this is not something 
I see as a good solution either.

^ permalink raw reply

* Re: [PATCH] index-pack, unpack-objects: increase input buffer from 4 KiB to 128 KiB
From: Junio C Hamano @ 2026-04-25 10:21 UTC (permalink / raw)
  To: Scott Bauersfeld via GitGitGadget; +Cc: git, Scott Bauersfeld
In-Reply-To: <pull.2282.git.git.1777058098756.gitgitgadget@gmail.com>

"Scott Bauersfeld via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Scott Bauersfeld <sbauersfeld@g.ucla.edu>
>
> Both index-pack and unpack-objects read pack data from stdin through
> a 4 KiB static buffer (input_buffer[4096]). On each fill(), consumed
> bytes are flushed to the output pack file via write_or_die(), so
> every write(2) moves at most 4 KiB.

Micronit.  Output of unpack-objects obviously does not get flushed
to "the output pack file".

> On FUSE-backed filesystems every write(2) is a synchronous round
> trip through the FUSE protocol (userspace -> kernel -> userspace ->
> back), so the 4 KiB buffer turns a clone into many unnecessary tiny
> writes with noticeable latency overhead.
>
> Increase the buffer from 4 KiB to 128 KiB, matching the default
> already used by the hashfile layer in csum-file.c.

Quite sensible reasoning presented very nicely.

It may probably be a #leftoverbit but these three instances of (128
* 1024) may want to have a common symbolic constant, like

    #define DEFAULT_IOBUFFER_SIZE_IN_BYTES (128 * 1024)

in a bit more central header file.  Especially for the one in
csum-file.c where there is no symbolic constant used for that
purpose.

> Testing with strace on HTTPS clones of git/git (~296 MB pack, 5 runs
> per variant, isolated builds from the same v2.54.0 source) shows:
>
>   index-pack pack file writes: 72,465 -> 24,943 avg (66% reduction)
>   total write() syscalls:     310,192 -> 259,530 avg (17% reduction)
>   writes of exactly 4096 bytes: ~40,077 -> 0 (eliminated)

Hmph, I would have expected more like (1 - 4/128) ~ 97% reduction.
The difference between that and 66% is coming from where?  There are
inherently short writes that do not utilize the new larger buffer
beyond 4kB?  If so, another number of interest might be the number
of writes smaller than 4096 bytes, perhaps?

^ permalink raw reply

* Re: [PATCH] [ci] Update GitHub Actions to latest major release / GitHub #2278
From: Johannes Schindelin @ 2026-04-25 10:22 UTC (permalink / raw)
  To: Christoph Grüninger; +Cc: git
In-Reply-To: <b3238847-91c1-42fa-b87a-b9d10daa6066@grueninger.de>

[-- Attachment #1: Type: text/plain, Size: 1289 bytes --]

Hi Christoph,

On Sat, 25 Apr 2026, Christoph Grüninger wrote:

> I repost my suggested contribution from:
> 
> https://github.com/git/git/pull/2278
> 
> I updated all GitHub Actions to their latest major release. In contrast 
> to "ci: GitHub Actions updates" (brought to you by Dependabot 
> (https://lore.kernel.org/git/pull.2097.git.1776775319.gitgitgadget@gmail.com/T/#t), 
> I update some more standard actions and mshick/add-pr-comment.
> They fix deprecation warnings that GitHub Action deprecated Node20.js.

Sorry, I was unclear in my suggestion at
https://github.com/git/git/pull/2278#issuecomment-4302349600: I did not
mean to just throw out a diverging patch. What I meant was to analyze what
the differences are, and to work toward a combined, improved contribution
that is better than either of the original ones.

Lucky for me, it is easy to compare the contributions via:
https://github.com/dscho/git/compare/dependabot-updates..gruenich:git:feature/update-github-actions

This diff shows that I had missed an update of the `mshick/add-pr-comment`
Action from v2 to v3. Or, more correctly, Dependabot had not yet reported
this in `git-for-windows/git`...

I will add a patch to my patch series, crediting you with the find.

Ciao,
Johannes

^ permalink raw reply

* Re: [PATCH] [ci] Update GitHub Actions to latest major release / GitHub #2278
From: Junio C Hamano @ 2026-04-25 10:37 UTC (permalink / raw)
  To: Christoph Grüninger; +Cc: git, johannes.schindelin
In-Reply-To: <b3238847-91c1-42fa-b87a-b9d10daa6066@grueninger.de>

Christoph Grüninger <foss@grueninger.de> writes:

> Dear Git developer!
>
> I repost my suggested contribution from:
>
> https://github.com/git/git/pull/2278


Glad to see new people show interest in joining the Git development
community.  Documentation/SubmittingPatches would help.

> I updated all GitHub Actions to their latest major release. In contrast 
> to "ci: GitHub Actions updates" (brought to you by Dependabot 
> (https://lore.kernel.org/git/pull.2097.git.1776775319.gitgitgadget@gmail.com/T/#t), 
> I update some more standard actions and mshick/add-pr-comment.
> They fix deprecation warnings that GitHub Action deprecated Node20.js.
>
> Kind regards,
> Christoph

Thanks.

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox