* Re: [PATCH v2 09/11] git-gui: allow specifying path '.' to the browser
From: Mark Levedahl @ 2026-05-23 15:43 UTC (permalink / raw)
To: Johannes Sixt; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <ae3cdc22-2f88-4222-bab7-403408373a53@kdbg.org>
On 5/23/26 10:23 AM, Johannes Sixt wrote:
> Am 20.05.26 um 22:24 schrieb Mark Levedahl:
>> Invoking "git-gui browser rev ." should show the file browser for the
>> commitish rev, starting at the current directory. When the current
>> directory is the working tree root, this errors out in normalize_relpath
>> because the '.' is removed, yielding an empty list as argument to [file
>> join ...]. The browser function demands "./" in this case, so make it
>> so. (./ works on Windows as well because g4w accepts posix file
>> naming).
> I wonder why we need "./" instead of plain ".". The latter works just
> fine in my tests (on Linux).
'.' caused errors in browser::new in for me before while './' worked, but now I find both
work. I'm confused, this must have been an interaction with something else in flight at
the time, will revert to '.' if that passes my tests on Windows as well as it is more
consistent of not adding '/' to a dirname.
Mark
^ permalink raw reply
* Re: [PATCH v2 07/11] git-gui: try harder to find worktree from gitdir
From: Mark Levedahl @ 2026-05-23 15:33 UTC (permalink / raw)
To: Johannes Sixt; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <a1e9da65-f8dd-4544-bbc9-d3b01328cebe@kdbg.org>
On 5/23/26 10:06 AM, Johannes Sixt wrote:
> I tried to come up with a situation where we end up here, but couldn't.
> When would this happen? If it actually can't happen, I would prefer to
> spawn fewer git processes and just take the result of 'file dirname'.
>
> If the code must remain, can we please rename one of gitdir_parent or
> parent_gitdir?
There are some odd cases I've seen, mostly driven by network file systems (using Samba or
NFS or ...) that don't behave as POSIX. Perhaps that is reasonably out of scope. The more
I think about this, I'll delete that part.
Symlinks can create odd cases:
Consider /tmp/main and /tmp/worktree. The latter has a .git entry that is a symlink to
/tmp/main.git. git rev-parse --absolute-git-dir shows
in dir /tmp/worktree/.git /tmp/main.git
So, git-gui would start in /tmp/main, and not in /tmp/worktree.
If using git new-workdir (which is still in the wild), this creates a .git dir with
symlinks to all the subdirs. Now git rev-parse reports
in dir /tmp/worktree.git /tmp/worktree/.git
in dir /tmp/worktree/.git/logs /tmp/main/.git
So, as long as this is started in the top level of .git, it's ok.
While the shell understands we descended into a symlink and reports pwd does not
de-referencing the symlink, tcl always dereferences the symlinks. So, any ability to
contain this behavior is very likely system and shell dependent.
So, in summary, I'll probably simplify the check to just --show-toplevel run in the dir
above .git.
Mark
^ permalink raw reply
* Re: [PATCH v3 6/8] promisor-remote: trust known remotes matching acceptFromServerUrl
From: Kristoffer Haugsbakk @ 2026-05-23 15:17 UTC (permalink / raw)
To: Christian Couder, git
Cc: Junio C Hamano, Patrick Steinhardt, Taylor Blau, Karthik Nayak,
Elijah Newren, Toon Claes, Christian Couder
In-Reply-To: <20260519153808.494105-7-christian.couder@gmail.com>
On Tue, May 19, 2026, at 17:38, Christian Couder wrote:
>[snip]
>
> Let's then use this helper in should_accept_remote() so that, a known
> remote whose URL matches the allowlist is accepted.
I don’t understand this comma break?
>
> To prepare for this new logic, let's also:
>
>[snip]
>
> Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
The rest of the commit message looks good to me.
> ---
> Documentation/config/promisor.adoc | 74 +++++++++++++++++++
> Documentation/gitprotocol-v2.adoc | 9 ++-
> promisor-remote.c | 102 +++++++++++++++++++++++---
> t/t5710-promisor-remote-capability.sh | 71 ++++++++++++++++++
> 4 files changed, 242 insertions(+), 14 deletions(-)
>
> diff --git a/Documentation/config/promisor.adoc
>[snip]
> ++
> +Be _VERY_ careful with these patterns: `*` matches any sequence of
> +characters within the 'host' and 'path' parts of a URL (but cannot
> +cross part boundaries). An overly broad pattern is a major security
> +risk, as a matching URL allows a server to update fields (such as
> +authentication tokens) on known remotes without further confirmation.
> +To minimize security risks, follow these guidelines:
> ++
So this introduces a list of precautions to take.
> +1. Start with a secure protocol scheme, like `https://` or `ssh://`.
> ++
> +2. Only allow domain names or paths where you control and trust _ALL_
> + the content. Be especially careful with shared hosting platforms
> + like `github.com` or `gitlab.com`. A broad pattern like
> + `https://gitlab.com/*` is dangerous because it trusts every
> + repository on the entire platform. Always restrict such patterns to
> + your specific organization or namespace (e.g.,
> + `https://gitlab.com/your-org/*`).
> ++
> +3. Never use globs at the end of domain names. For example,
> + `https://cdn.your-org.com/*` might be safe, but
> + `https://cdn.your-org.com*/*` is a major security risk because
> + the latter matches `https://cdn.your-org.com.hacker.net/repo`.
> ++
> +4. Be careful using globs at the beginning of domain names. While the
> + code ensures a `*` in the host cannot cross into the path, a
> + pattern like `https://*.example.com/*` will still match any
> + subdomain. This is extremely dangerous on shared hosting platforms
> + (e.g., `https://*.github.io/*` trusts every user's site on the
> + entire platform).
The list seems to end here, because...
> ++
> +Before matching, both the advertised URL and the pattern are
> +normalized: the scheme and host are lowercased, percent-encoded
This next paragraph seems to go back to describing how things work. But
this paragraph as well as all of the following ones belong to this list
item:
4. Be careful using globs [...]
Before matching, [...]
The glob pattern can [...]
If a remote with the [...]
For the security implications [...]
promisor.checkFields
[...]
I don’t know what the intent is. But using an open block will delimit
the ordered list.
diff --git Documentation/config/promisor.adoc Documentation/config/promisor.adoc
index cc728bb0b5e..f07a2e883bd 100644
--- Documentation/config/promisor.adoc
+++ Documentation/config/promisor.adoc
@@ -109,6 +109,7 @@ and to update fields (such as authentication tokens) on known remotes
without further confirmation. To minimize security risks, follow these
guidelines:
+
+--
1. Start with a secure protocol scheme, like `https://` or `ssh://`.
+
2. Only allow domain names or paths where you control and trust _ALL_
@@ -130,6 +131,7 @@ guidelines:
subdomain. This is extremely dangerous on shared hosting platforms
(e.g., `https://*.github.io/*` trusts every user's site on the
entire platform).
+--
+
Before matching, both the advertised URL and the pattern are
normalized: the scheme and host are lowercased, percent-encoded
>[snip]
^ permalink raw reply
* Re: [PATCH v2 09/11] git-gui: allow specifying path '.' to the browser
From: Johannes Sixt @ 2026-05-23 14:23 UTC (permalink / raw)
To: Mark Levedahl; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <20260520202411.108764-10-mlevedahl@gmail.com>
Am 20.05.26 um 22:24 schrieb Mark Levedahl:
> Invoking "git-gui browser rev ." should show the file browser for the
> commitish rev, starting at the current directory. When the current
> directory is the working tree root, this errors out in normalize_relpath
> because the '.' is removed, yielding an empty list as argument to [file
> join ...]. The browser function demands "./" in this case, so make it
> so. (./ works on Windows as well because g4w accepts posix file
> naming).
I wonder why we need "./" instead of plain ".". The latter works just
fine in my tests (on Linux).
>
> Signed-off-by: Mark Levedahl <mlevedahl@gmail.com>
> ---
> git-gui.sh | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/git-gui.sh b/git-gui.sh
> index a72d8a59ec..d373457901 100755
> --- a/git-gui.sh
> +++ b/git-gui.sh
> @@ -3007,7 +3007,11 @@ proc normalize_relpath {path} {
> }
> lappend elements $item
> }
> - return [eval file join $elements]
> + if {$elements ne {}} {
> + return [eval file join $elements]
> + } else {
> + return {./}
> + }
> }
>
> # -- Not a normal commit type invocation? Do that instead!
-- Hannes
^ permalink raw reply
* Re: [PATCH v2 08/11] git-gui: use HEAD as current branch when detached (bug fix)
From: Johannes Sixt @ 2026-05-23 14:08 UTC (permalink / raw)
To: Mark Levedahl, git; +Cc: egg_mushroomcow, bootaina702
In-Reply-To: <20260520202411.108764-9-mlevedahl@gmail.com>
Am 20.05.26 um 22:24 schrieb Mark Levedahl:
> commit f87a36b697 ("git-gui: use git-branch --show-current", 2024-02-12)
> changed git-gui to use git-branch to access refs, rather than directly
> reading files as doing the latter is not compatible with the reftable
> backend. git branch --show-current reports an empty branch name when the
> head is detached, and in this case load_current_branch needs to report
> HEAD using special case logic as it did prior to the above commit. Make
> it do so.
>
> This addresses an issue with git-gui browser failing with a detached
> head.
Nice catch. I'll reorder this as the first commit.
>
> Signed-off-by: Mark Levedahl <mlevedahl@gmail.com>
> ---
> git-gui.sh | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/git-gui.sh b/git-gui.sh
> index aeb7ed3548..a72d8a59ec 100755
> --- a/git-gui.sh
> +++ b/git-gui.sh
> @@ -648,6 +648,9 @@ proc load_current_branch {} {
>
> set current_branch [git branch --show-current]
> set is_detached [expr [string length $current_branch] == 0]
> + if {$is_detached} {
> + set current_branch {HEAD}
> + }
> }
>
> auto_load tk_optionMenu
-- Hannes
^ permalink raw reply
* Re: [PATCH v2 07/11] git-gui: try harder to find worktree from gitdir
From: Johannes Sixt @ 2026-05-23 14:06 UTC (permalink / raw)
To: Mark Levedahl; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <20260520202411.108764-8-mlevedahl@gmail.com>
Am 20.05.26 um 22:24 schrieb Mark Levedahl:
> git-gui, since 87cd09f43e ("git-gui: work from the .git dir",
> 2010-01-23), has had the intent to allow starting from inside a
> repository, then switching to the parent directory if that is a valid
> worktree.
I can imagine that this kind of use occurs in "Git GUI here" menu item
of the file explorer on Windows. So, we should resurrect the feature.
>
> This certainly hasn't worked since 2d92ab32fd ("rev-parse: make
> --show-toplevel without a worktree an error", 2019-11-19) in git, but
> breaking this git-gui feature was unintentional.
>
> There are (at least) 3 cases where the gitdir can tell us where the
> worktree is, and we would like all to work:
>
> - core.worktree is set, and points to a valid worktree. This is already
> handled by git rev-parse --show-toplevel, even when not in the worktree.
> There is nothing more to do in this case.
>
> - the gitdir is embedded in a worktree as subdirectory .git. The parent
> is (or at least should be) a valid worktree. This worked long ago.
>
> - the gitdir is a worktree specific directory (under
> <mainrepo>/worktrees/worktree_name), within which there is a file
> "gitdir" pointing to .git in the worktree. git gui never learned to
> handle this case.
>
> Let's handle the latter two cases. Always check that the discovered
> worktree is valid and points to the already discovered gitdir according
> to git rev-parse. This avoids issues that may arise because we are
> discovering from the gitdir up, rather than the worktree down, and file
> system non-posix behavior or misconfiguration of git might cause
> confusion. For instance, a manually moved worktree might not be where
> the gitdir points.
>
> Signed-off-by: Mark Levedahl <mlevedahl@gmail.com>
> ---
> git-gui.sh | 42 ++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 42 insertions(+)
>
> diff --git a/git-gui.sh b/git-gui.sh
> index 8fe25fe188..aeb7ed3548 100755
> --- a/git-gui.sh
> +++ b/git-gui.sh
> @@ -1100,6 +1100,41 @@ unset argv0dir
> ##
> ## repository setup
>
> +proc find_worktree_from_gitdir {} {
> + # Directory 'parent' of a repository named 'parent/.git' might be the worktree.
> + # Assure parent is a worktree and using the git repository already discovered.
> + # Also, handle case of being in a worktree's gitdir, where file "gitdir" points to
> + # gitlink file .git in the real worktree.
> + set worktree {}
> + if {[file tail $::_gitdir] eq {.git}} {
> + if {[catch {
> + set gitdir_parent [file dirname $::_gitdir]
> + set worktree [git -C $gitdir_parent rev-parse --show-toplevel]
> + set parent_gitdir [git -C $worktree rev-parse --absolute-git-dir]
> + if {$::_gitdir ne $parent_gitdir} {
> + set worktree {}
I tried to come up with a situation where we end up here, but couldn't.
When would this happen? If it actually can't happen, I would prefer to
spawn fewer git processes and just take the result of 'file dirname'.
If the code must remain, can we please rename one of gitdir_parent or
parent_gitdir?
> + }
> + }]} {
> + set worktree {}
> + }
> + } elseif [file exists {gitdir}] {
> + if {[catch {
> + set fd_gitdir [open {gitdir} {r}]
> + set gitlink_parent [file dirname [read $fd_gitdir]]
> + catch {close $fd_gitdir}
> + set worktree [git -C $gitlink_parent rev-parse --show-toplevel]
> + set parent_gitdir [git -C $worktree rev-parse --absolute-git-dir]
Since worktrees can be messed up quite easily, it looks reasonable to
check whether the worktree points back to the gitdir. (But I haven't
tried to construct a case that passes the check in the next line.)
> + if {$::_gitdir ne $parent_gitdir} {
> + set worktree {}
> + }
> + }]} {
> + catch {close $fd_gitdir}
> + set worktree {}
> + }
> + }
> + return $worktree
> +}
> +
> proc is_gitvars_error {err} {
> set havevars 0
> set GIT_DIR {}
> @@ -1176,6 +1211,13 @@ if {[catch {
> set _prefix {}
> }
>
> +if {[is_bare]} {
> + # Maybe we are in an embedded or worktree specific gitdir
> + if {[set _gitworktree [find_worktree_from_gitdir]] ne {}} {
> + set _prefix {}
> + }
> +}
> +
> if {![is_bare]} {
> if {[catch {
> cd $_gitworktree
-- Hannes
^ permalink raw reply
* Re: [PATCH v2 06/11] git-gui: use git rev-parse for worktree discovery
From: Johannes Sixt @ 2026-05-23 13:26 UTC (permalink / raw)
To: Mark Levedahl; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <20260520202411.108764-7-mlevedahl@gmail.com>
Am 20.05.26 um 22:24 schrieb Mark Levedahl:
> git gui uses a combination of tcl code and git invocations to determine
> the worktree and the location with respect to the worktree root
> (_prefix). But, git rev-parse provides all of this information directly,
> and assures full error and configuration checking are done by git
> itself. The entirety of discovery in normal configurations involves
>
> git rev-parse --show-toplevel (gets worktree root)
> git rev-parse --show-prefix (shows location wrt the root)
>
> An error thrown on either of these lines means the worktree discovered
> by git is unusable, or git did not discover a worktree because the
> current directory is inside the repository. If the user has defined
> GIT_DIR or GIT_WORK_TREE, this is a user configuration error and git-gui
> should stop.
>
> Otherwise, the blame or browser subcommands can be used without a
> worktree.
>
> A separate error might occur when changing to the root of the discovered
> worktree. The cause would be file system related and completely outside
> of git's control. So, the final "cd $worktree_root" is separately
> trapped.
>
> Discovery of the repository and the worktree must be guarded to trap
> errors: the intent is that any configuration problems are caught during
> discovery, and later processing need not include error trapping and
> recovery. So, move all worktree discovery code to be immediately after
> repository discovery.
>
> This does move configuration loading to occur after worktree discovery
> rather than before. None of the code executed in worktree discovery has
> any option controlled by a git-gui configuration variable, so no impact
> is expected. git itself will always read the repository configuration,
> including worktree specific configuration data if that exists, so this
> is unaffected by when git-gui loads its own config data, and we cannot
> be sure the full worktree dependent configuration can be loaded before
> full discovery is complete.
Very good!
When you move code around, please do not apply style changes so that
git show --color-moved --color-moved-ws=allow-indentation-change
can prove that no change was intended.
>
> Signed-off-by: Mark Levedahl <mlevedahl@gmail.com>
> ---
> git-gui.sh | 64 +++++++++++++++++++++++++-----------------------------
> 1 file changed, 30 insertions(+), 34 deletions(-)
>
> diff --git a/git-gui.sh b/git-gui.sh
> index 936c309e59..8fe25fe188 100755
> --- a/git-gui.sh
> +++ b/git-gui.sh
> @@ -1164,6 +1164,36 @@ if {$_gitdir eq {}} {
> set picked 1
> }
>
> +# find worktree, continue without if not required
> +if {[catch {
> + set _gitworktree [git rev-parse --show-toplevel]
> + set _prefix [git rev-parse --show-prefix]
> +} err]} {
> + if {[is_gitvars_error $err]} {
> + exit 1
> + }
> + set _gitworktree {}
> + set _prefix {}
> +}
> +
> +if {![is_bare]} {
> + if {[catch {
> + cd $_gitworktree
> + } err]} {
> + catch {wm withdraw .}
> + error_popup [strcat [mc "Cannot change to discovered worktree: "] \
> + "$_gitworktree" "\n\n$err"]
> + exit 1;
> + }
> +} elseif {![is_enabled bare]} {
> + catch {wm withdraw .}
> + error_popup [strcat [mc "Cannot use bare repository:"] "\n\n" $_gitdir]
> + exit 1
> +}
> +
> +# repository and worktree config are complete, export them
> +set_gitdir_vars
> +
> # Use object format as hash algorithm (either "sha1" or "sha256")
> set hashalgorithm [git rev-parse --show-object-format]
> if {$hashalgorithm eq "sha1"} {
> @@ -1179,37 +1209,6 @@ if {$hashalgorithm eq "sha1"} {
> load_config 0
> apply_config
>
> -set _gitworktree [git rev-parse --show-toplevel]
> -
> -if {$_prefix ne {}} {
> - if {$_gitworktree eq {}} {
> - regsub -all {[^/]+/} $_prefix ../ cdup
> - } else {
> - set cdup $_gitworktree
> - }
> - if {[catch {cd $cdup} err]} {
> - catch {wm withdraw .}
> - error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"]
> - exit 1
> - }
> - set _gitworktree [pwd]
> - unset cdup
> -} elseif {![is_enabled bare]} {
> - if {[is_bare]} {
> - catch {wm withdraw .}
> - error_popup [strcat [mc "Cannot use bare repository:"] "\n\n$_gitdir"]
> - exit 1
> - }
> - if {$_gitworktree eq {}} {
> - set _gitworktree [file dirname $_gitdir]
> - }
> - if {[catch {cd $_gitworktree} err]} {
> - catch {wm withdraw .}
> - error_popup [strcat [mc "No working directory"] " $_gitworktree:\n\n$err"]
> - exit 1
> - }
> - set _gitworktree [pwd]
> -}
> set _reponame [file split [file normalize $_gitdir]]
> if {[lindex $_reponame end] eq {.git}} {
> set _reponame [lindex $_reponame end-1]
> @@ -1217,9 +1216,6 @@ if {[lindex $_reponame end] eq {.git}} {
> set _reponame [lindex $_reponame end]
> }
>
> -# Export the final paths
> -set_gitdir_vars
> -
> ######################################################################
> ##
> ## global init
-- Hannes
^ permalink raw reply
* Re: [PATCH v2 07/11] git-gui: try harder to find worktree from gitdir
From: Shroom Moo @ 2026-05-23 11:47 UTC (permalink / raw)
To: Mark Levedahl, Johannes Sixt; +Cc: git, Aina Boot
In-Reply-To: <c8d1ab1e-e0cb-44e2-afcd-728b7b43774c@kdbg.org>
On 5/23/26 4:01 PM, Johannes Sixt wrote:
> Am 21.05.26 um 06:55 schrieb Shroom Moo:
>> On 5/21/26 4:24 AM, Mark Levedahl wrote:
>>> + } elseif [file exists {gitdir}] {
>>> + if {[catch {
>>> + set fd_gitdir [open {gitdir} {r}]
>>> + set gitlink_parent [file dirname [read $fd_gitdir]]
>>> + catch {close $fd_gitdir}
>>> + set worktree [git -C $gitlink_parent rev-parse --show-toplevel]
>>> + set parent_gitdir [git -C $worktree rev-parse --absolute-git-dir]
>>> + if {$::_gitdir ne $parent_gitdir} {
>>> + set worktree {}
>>> + }
>>> + }]} {
>>> + catch {close $fd_gitdir}
>>> + set worktree {}
>>> + }
>>> + }
>> Additionally, [file exists {gitdir}] checks for the gitdir file in
>> the current working directory. Since the function has not yet
>> switched to $_gitdir when this check runs, it is almost impossible
>> to find the file. Consequently, this logic never triggers, preventing
>> linked worktrees from being recognized.
> I think you are misunderstanding which use-case this code is addressing.
> The case can be triggered very easily.
> First, the code before the part we see above is intended for the special
> case where we start in a .git, where `--show-toplevel` bails out and we
> define the worktree to be the directory containing .git.
> However, if we start in .git/worktrees/feature, then the code cited
> above kicks in, because `--show-toplevel` still bails out,
> `--absolute-git-dir` does not end in '.git', but now we have a file
> named 'gitdir' in the current directory. In this case, we define (and
> this is new with this patch) that the worktree is the one where the
> 'gitdir' points.
>
> -- Hannes
I see. The condition is unrelated to this patch. Users should handle
this case as assigning manually by rule. Indeed we don't need to
modify it.
Shroom
^ permalink raw reply
* Re: [PATCH 0/4] doc: hook: small improvements
From: Jean-Noël AVILA @ 2026-05-23 10:24 UTC (permalink / raw)
To: git, kristofferhaugsbakk; +Cc: Kristoffer Haugsbakk, adrian.ratiu
In-Reply-To: <CV_doc_hook.6f0@msgid.xyz>
On Thursday, 21 May 2026 18:25:54 CEST kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Topic name: kh/doc-hook
>
> Topic summary: Small improvements to git-hook(1) and the associated config.
>
> [1/4] doc: hook: remove stray backtick
> [2/4] doc: hook: consistently capitalize Git
> [3/4] doc: config: include existing git-hook(1) section
> [4/4] doc: hook: don’t self-link via config include
>
> Documentation/config.adoc | 2 ++
> Documentation/config/hook.adoc | 19 +++++++++++++------
> Documentation/git-hook.adoc | 11 ++++++-----
> 3 files changed, 21 insertions(+), 11 deletions(-)
>
>
> base-commit: aec3f587505a472db67e9462d0702e7d463a449d
This series looks good to me.
Thanks
^ permalink raw reply
* Re: [PATCH v2 01/11] git-gui: guard set/unset of GIT_DIR and GIT_WORK_TREE
From: Aina Boot @ 2026-05-23 11:46 UTC (permalink / raw)
To: Johannes Sixt, Mark Levedahl; +Cc: Shroom Moo, git
In-Reply-To: <b332c7d9-c86b-4d4b-a873-1600d910a237@kdbg.org>
On 5/23/26 8:19 AM, Johannes Sixt wrote:
> The other patch that removes cd $_gitworktree from do_gitk should still
> be good, I think.
Agree. It either succeededly set the directory or work without a
worktree, cd is practically unnecessary.
Aina
^ permalink raw reply
* Re: [PATCH 0/4] doc: hook: small improvements
From: Kristoffer Haugsbakk @ 2026-05-23 11:43 UTC (permalink / raw)
To: Jean-Noël AVILA, git; +Cc: Adrian Ratiu
In-Reply-To: <2832179.mvXUDI8C0e@piment-oiseau>
On Sat, May 23, 2026, at 12:24, Jean-Noël AVILA wrote:
> On Thursday, 21 May 2026 18:25:54 CEST kristofferhaugsbakk@fastmail.com wrote:
>> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>>
>> Topic name: kh/doc-hook
>>
>> Topic summary: Small improvements to git-hook(1) and the associated config.
>>
>> [1/4] doc: hook: remove stray backtick
>> [2/4] doc: hook: consistently capitalize Git
>> [3/4] doc: config: include existing git-hook(1) section
>> [4/4] doc: hook: don’t self-link via config include
>>
>> Documentation/config.adoc | 2 ++
>> Documentation/config/hook.adoc | 19 +++++++++++++------
>> Documentation/git-hook.adoc | 11 ++++++-----
>> 3 files changed, 21 insertions(+), 11 deletions(-)
>>
>>
>> base-commit: aec3f587505a472db67e9462d0702e7d463a449d
>
> This series looks good to me.
Thanks. Can I add your ack to the patches?
^ permalink raw reply
* [PATCH 2/2] t2000: cleanup unused debug code and variables
From: Zakariyah Ali via GitGitGadget @ 2026-05-23 11:07 UTC (permalink / raw)
To: git
Cc: Christian Couder, Karthik Nayak, Justin Tobler, Siddharth Asthana,
Ayush Chandekar, Zakariyah Ali, Zakariyah Ali
In-Reply-To: <pull.2256.git.git.1779534462.gitgitgadget@gmail.com>
From: Zakariyah Ali <zakariyahali100@gmail.com>
Remove the show_files function which is no longer used after removing
test_debug calls, and remove an unused tree3 variable assignment in
the second test scenario.
These cleanups address feedback from Junio C Hamano regarding the
modernization of this test script.
Signed-off-by: Zakariyah Ali <zakariyahali100@gmail.com>
---
t/t2000-conflict-when-checking-files-out.sh | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/t/t2000-conflict-when-checking-files-out.sh b/t/t2000-conflict-when-checking-files-out.sh
index 43ec901f9e..7b61370549 100755
--- a/t/t2000-conflict-when-checking-files-out.sh
+++ b/t/t2000-conflict-when-checking-files-out.sh
@@ -23,17 +23,6 @@ test_description='git conflicts when checking files out test.'
. ./test-lib.sh
-show_files() {
- # show filesystem files, just [-dl] for type and name
- find path? -ls |
- sed -e 's/^[0-9]* * [0-9]* * \([-bcdl]\)[^ ]* *[0-9]* *[^ ]* *[^ ]* *[0-9]* [A-Z][a-z][a-z] [0-9][0-9] [^ ]* /fs: \1 /'
- # what's in the cache, just mode and name
- git ls-files --stage |
- sed -e 's/^\([0-9]*\) [0-9a-f]* [0-3] /ca: \1 /'
- # what's in the tree, just mode and name.
- git ls-tree -r "$1" |
- sed -e 's/^\([0-9]*\) [^ ]* [0-9a-f]* /tr: \1 /'
-}
test_expect_success 'prepare files path0 and path1/file1' '
date >path0 &&
@@ -96,7 +85,6 @@ test_expect_success 'checkout-index -f resolves symlink conflict on leading path
git read-tree -m $tree1 &&
git checkout-index -f -a &&
test_ln_s_add path2 path3 &&
- tree3=$(git write-tree) &&
git read-tree $tree2 &&
git checkout-index -f -a &&
test_path_is_dir_not_symlink path2 &&
--
gitgitgadget
^ permalink raw reply related
* [PATCH 1/2] t2000: consolidate second scenario into a single test block
From: Zakariyah Ali via GitGitGadget @ 2026-05-23 11:07 UTC (permalink / raw)
To: git
Cc: Christian Couder, Karthik Nayak, Justin Tobler, Siddharth Asthana,
Ayush Chandekar, Zakariyah Ali, Zakariyah Ali
In-Reply-To: <pull.2256.git.git.1779534462.gitgitgadget@gmail.com>
From: Zakariyah Ali <zakariyahali100@gmail.com>
Now that the test script has been modernised, consolidate the eight
separate test_expect_success blocks that together form the second
test scenario (setup, tree writes, checkout, symlink creation, and
final state check) into one self-contained block.
This makes it easier to read: data set-up, the operations being
tested, and the expected outcome are now all in one place.
Helped-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Zakariyah Ali <zakariyahali100@gmail.com>
---
t/t2000-conflict-when-checking-files-out.sh | 55 ++++-----------------
1 file changed, 9 insertions(+), 46 deletions(-)
diff --git a/t/t2000-conflict-when-checking-files-out.sh b/t/t2000-conflict-when-checking-files-out.sh
index af199d8191..43ec901f9e 100755
--- a/t/t2000-conflict-when-checking-files-out.sh
+++ b/t/t2000-conflict-when-checking-files-out.sh
@@ -83,59 +83,22 @@ test_expect_success SYMLINKS 'checkout-index -f twice with --prefix' '
# path path3 is occupied by a non-directory. With "-f" it should remove
# the symlink path3 and create directory path3 and file path3/file1.
-test_expect_success 'prepare path2/file0 and index' '
+test_expect_success 'checkout-index -f resolves symlink conflict on leading path' '
mkdir path2 &&
date >path2/file0 &&
- git update-index --add path2/file0
-'
-
-test_expect_success 'write tree with path2/file0' '
- tree1=$(git write-tree)
-'
-
-test_debug 'show_files $tree1'
-
-test_expect_success 'prepare path3/file1 and index' '
+ git update-index --add path2/file0 &&
+ tree1=$(git write-tree) &&
mkdir path3 &&
date >path3/file1 &&
- git update-index --add path3/file1
-'
-
-test_expect_success 'write tree with path3/file1' '
- tree2=$(git write-tree)
-'
-
-test_debug 'show_files $tree2'
-
-test_expect_success 'read previously written tree and checkout.' '
+ git update-index --add path3/file1 &&
+ tree2=$(git write-tree) &&
rm -fr path3 &&
git read-tree -m $tree1 &&
- git checkout-index -f -a
-'
-
-test_debug 'show_files $tree1'
-
-test_expect_success 'add a symlink' '
- test_ln_s_add path2 path3
-'
-
-test_expect_success 'write tree with symlink path3' '
- tree3=$(git write-tree)
-'
-
-test_debug 'show_files $tree3'
-
-# Morten says "Got that?" here.
-# Test begins.
-
-test_expect_success 'read previously written tree and checkout.' '
+ git checkout-index -f -a &&
+ test_ln_s_add path2 path3 &&
+ tree3=$(git write-tree) &&
git read-tree $tree2 &&
- git checkout-index -f -a
-'
-
-test_debug 'show_files $tree2'
-
-test_expect_success 'checking out conflicting path with -f' '
+ git checkout-index -f -a &&
test_path_is_dir_not_symlink path2 &&
test_path_is_dir_not_symlink path3 &&
test_path_is_file_not_symlink path2/file0 &&
--
gitgitgadget
^ permalink raw reply related
* [PATCH 0/2] [GSoC Patch] t2000: modernize path checks to use helper functions
From: Zakariyah Ali via GitGitGadget @ 2026-05-23 11:07 UTC (permalink / raw)
To: git
Cc: Christian Couder, Karthik Nayak, Justin Tobler, Siddharth Asthana,
Ayush Chandekar, Zakariyah Ali
This is my GSoC microproject submission modernizing test path checks in
t/t2000-conflict-when-checking-files-out.sh.
Replace old-style path checks using test -f, test -d, and test ! -h with
dedicated test helper functions for improved test clarity and consistency.
This modernization improves test script readability by using Git's dedicated
test helpers:
test -f → test_path_is_file test -d → test_path_is_dir test ! -h && test -f
→ test_path_is_file_not_symlink test ! -h && test -d →
test_path_is_dir_not_symlink Found instances using: git grep 'test -[efd]'
t/ | grep 'test -[efd].*&&'
Converted 5 instances in t/t2000-conflict-when-checking-files-out.sh
This improves test clarity and consistency across the test suite.
I'm excited to contribute to Git and look forward to your feedback!
Zakariyah Ali (2):
t2000: consolidate second scenario into a single test block
t2000: cleanup unused debug code and variables
t/t2000-conflict-when-checking-files-out.sh | 65 +++------------------
1 file changed, 8 insertions(+), 57 deletions(-)
base-commit: 60f07c4f5c5f81c8a994d9e06b31a4a3a1679864
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2256%2Falibaba0010%2Fmodernize-test-path-checking-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2256/alibaba0010/modernize-test-path-checking-v1
Pull-Request: https://github.com/git/git/pull/2256
--
gitgitgadget
^ permalink raw reply
* [PATCH v3 4/4] notes: support an external command to display notes
From: Siddh Raman Pant @ 2026-05-23 10:38 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Elijah Newren, brian m. carlson, Jeff King, Johannes Sixt,
Oswald Buddenhagen
In-Reply-To: <cover.1779532562.git.siddh.raman.pant@oracle.com>
git notes is a very very helpful feature to show user-supplied
information about a commit alongside its message transparently.
For distributed teams working on large git repos (huge number of
branches/refs, files, etc.) and using the notes feature to mark
information on git commits, the problem is often not that two users
update the same note object at the same time. It is that the local
notes state used while reading history can be stale.
In kernel work, the same logical upstream fix can appear as different
commit objects across many downstream branches, such as the stable
branches and vendor-specific branches (based on which the released
kernel is actually built). Different developers may be working on those
branches in parallel, and a review decision recorded for one backport
is useful context for the others.
Today, seeing that decision in ordinary history output requires first
synchronizing the local notes ref, and then interpreting those notes
for the branch being inspected. The latter step is workflow-specific
and can be cheap, but keeping the local notes state fresh enough can be
expensive in a large kernel repository with a large shared notes
history (and if we are to extrapolate, a slow git server conn/ops can
be a factor too).
This TOCTOU problem exacerbates on scale (rapid updates, more devs,
larger repos, more git server traffic, etc).
One solution to this is to move the freshness policy out of git so that
it is someone else's problem. We can have a realtime fetch or faster
updation via external helper means. But unfortunately we lose the
coherence in the display of information, and so the user would end up
reinventing git log in his quest to have same workflow.
Let's add support for notes.externalCommand, a protected-configuration
command that git runs as a long-lived helper when displaying notes. git
sends commit IDs to the helper and displays any returned text through
the existing notes formatting path. This keeps presentation in git
while letting the helper decide how fresh note text is obtained.
We also add configuration for the displayed notes header name, timeout
enforcement for the helper so that git doesn't hang waiting on it,
optional --grep participation, and command-line controls to enable or
disable external notes. The new help text should make the usage clear.
Assisted-by: Codex:gpt-5.5-xhigh-fast
Signed-off-by: Siddh Raman Pant <siddh.raman.pant@oracle.com>
---
Documentation/config/notes.adoc | 59 +++
Documentation/git-format-patch.adoc | 11 +-
Documentation/git-range-diff.adoc | 6 +
Documentation/pretty-options.adoc | 9 +
Makefile | 2 +
builtin/log.c | 17 +-
builtin/name-rev.c | 9 +-
builtin/range-diff.c | 2 +
contrib/completion/git-completion.bash | 4 +-
log-tree.c | 7 +-
meson.build | 1 +
notes-external.c | 414 +++++++++++++++++++
notes-external.h | 53 +++
notes.c | 264 +++++++++---
notes.h | 33 +-
revision.c | 36 +-
t/helper/meson.build | 1 +
t/helper/test-external-notes | 64 +++
t/helper/test-notes-external-config-reset.c | 24 ++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/lib-notes.sh | 19 +
t/t3206-range-diff.sh | 68 ++++
t/t3301-notes.sh | 424 ++++++++++++++++++++
t/t6120-describe.sh | 17 +
25 files changed, 1483 insertions(+), 63 deletions(-)
create mode 100644 notes-external.c
create mode 100644 notes-external.h
create mode 100755 t/helper/test-external-notes
create mode 100644 t/helper/test-notes-external-config-reset.c
create mode 100644 t/lib-notes.sh
diff --git a/Documentation/config/notes.adoc b/Documentation/config/notes.adoc
index b7e536496f51..023ec6c5d8d1 100644
--- a/Documentation/config/notes.adoc
+++ b/Documentation/config/notes.adoc
@@ -34,6 +34,65 @@ The effective value of `core.notesRef` (possibly overridden by
`GIT_NOTES_REF`) is also implicitly added to the list of refs to be
displayed.
+`notes.externalCommand`::
+ Command to invoke as a long-lived helper when showing commit messages
+ with the `git log` family of commands. Git sends one commit object ID
+ per request on the command's standard input:
++
+------------
+<hex-commit-id>
+------------
++
+For each request, the helper must respond on its standard output with either
+`<hex-commit-id> missing` followed by a newline, or `<hex-commit-id> ok <n>`
+followed by a newline and exactly `<n>` bytes of UTF-8 note text followed by a
+newline. The helper must respond to each request as it is received; Git does
+not send all commit object IDs before reading responses. Empty note text is not
+displayed. External notes are only used while formatting output by default; see
+`notes.externalCommandForGrep` to include them when matching commits.
++
+If Git cannot start or communicate with the helper, or the helper sends an
+invalid response, Git warns once and disables it for the rest of the Git run.
+The process is closed using SIGTERM, so the helper should not trap it.
++
+This setting is only respected in protected configuration (see
+linkgit:git-config[1]). This prevents untrusted repositories from running
+arbitrary commands when notes are displayed.
++
+This setting does not take effect when:
++
+--
+* the value is empty;
+* `--no-notes` is given;
+* `--no-external-notes` is given; or
+* `--notes=<ref>` is given by itself without `--external-notes` or `--notes`.
+--
+
+`notes.externalCommandName`::
+ Name to use in the `Notes (<name>):` header for notes returned by
+ `notes.externalCommand`. Defaults to `external`. This setting is only
+ respected in protected configuration.
+
+`notes.externalCommandTimeoutMs`::
+ Number of milliseconds to wait when reading each response from
+ `notes.externalCommand`. Defaults to `100`. If the command does not
+ produce the expected response in time, Git warns once and disables it
+ for the rest of the command. A value of `0` disables timeout handling,
+ so reads can block until the command writes output or exits. This
+ setting is only respected in protected configuration.
+
+`notes.externalCommandForGrep`::
+ Boolean indicating whether notes returned by `notes.externalCommand`
+ are included when matching commits with `--grep`, wherever notes would
+ normally participate in grep matching. Defaults to false. This does
+ not make hidden notes searchable in formats such as `--oneline` or
+ `--pretty=%s`; use `--notes` or `--external-notes` if those formats
+ should search notes too. When enabled, revision traversal may invoke
+ the external command for many commits that are not ultimately
+ displayed, which can be expensive for slow commands. The note output
+ can also change which commits match. This setting is only respected in
+ protected configuration.
+
`notes.rewrite.<command>`::
When rewriting commits with _<command>_ (currently `amend` or
`rebase`), if this variable is `false`, git will not copy
diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc
index 566238245028..472b37e5237a 100644
--- a/Documentation/git-format-patch.adoc
+++ b/Documentation/git-format-patch.adoc
@@ -26,7 +26,7 @@ SYNOPSIS
[--[no-]cover-letter] [--quiet]
[--commit-list-format=<format-spec>]
[--[no-]encode-email-headers]
- [--no-notes | --notes[=<ref>]]
+ [--no-notes | --notes[=<ref>]] [--[no-]external-notes]
[--interdiff=<previous>]
[--range-diff=<previous> [--creation-factor=<percent>]]
[--filename-max-length=<n>]
@@ -395,6 +395,15 @@ configuration options in linkgit:git-notes[1] to use this workflow).
The default is `--no-notes`, unless the `format.notes` configuration is
set.
+--external-notes::
+--no-external-notes::
+ Invoke or do not invoke `notes.externalCommand` to obtain external
+ notes. Like `--notes=<ref>`, `--external-notes` names an explicit
+ note source and by itself does not include the default notes refs.
+ Use `--external-notes --notes` to include the default notes refs
+ too, or combine `--external-notes` with `--notes=<ref>` to include
+ external notes with specific notes refs.
+
--signature=<signature>::
--no-signature::
Add a signature to each message produced. Per RFC 3676 the signature
diff --git a/Documentation/git-range-diff.adoc b/Documentation/git-range-diff.adoc
index 5cc5e2ed5673..1de23f300517 100644
--- a/Documentation/git-range-diff.adoc
+++ b/Documentation/git-range-diff.adoc
@@ -12,6 +12,7 @@ git range-diff [--color=[<when>]] [--no-color] [<diff-options>]
[--no-dual-color] [--creation-factor=<factor>]
[--left-only | --right-only] [--diff-merges=<format>]
[--remerge-diff] [--no-notes | --notes[=<ref>]]
+ [--[no-]external-notes]
( <range1> <range2> | <rev1>...<rev2> | <base> <rev1> <rev2> )
[[--] <path>...]
@@ -101,6 +102,11 @@ diff.
This flag is passed to the `git log` program
(see linkgit:git-log[1]) that generates the patches.
+`--external-notes`::
+`--no-external-notes`::
+ This flag is passed to the `git log` program
+ (see linkgit:git-log[1]) that generates the patches.
+
`<range1> <range2>`::
Compare the commits specified by the two ranges, where
_<range1>_ is considered an older version of _<range2>_.
diff --git a/Documentation/pretty-options.adoc b/Documentation/pretty-options.adoc
index 658e462b2533..aad851c92cfd 100644
--- a/Documentation/pretty-options.adoc
+++ b/Documentation/pretty-options.adoc
@@ -93,6 +93,15 @@ being displayed. Examples: "`--notes=foo`" will show only notes from
"`--notes --notes=foo --no-notes --notes=bar`" will only show notes
from `refs/notes/bar`.
+`--external-notes`::
+`--no-external-notes`::
+ Invoke or do not invoke `notes.externalCommand` to obtain external
+ notes. Like `--notes=<ref>`, `--external-notes` names an explicit
+ note source and by itself does not include the default notes refs.
+ Use `--external-notes --notes` to include the default notes refs
+ too, or combine `--external-notes` with `--notes=<ref>` to include
+ external notes with specific notes refs.
+
`--show-notes-by-default`::
Show the default notes unless options for displaying specific
notes are given.
diff --git a/Makefile b/Makefile
index fb50c57e4f25..898da8936e84 100644
--- a/Makefile
+++ b/Makefile
@@ -834,6 +834,7 @@ TEST_BUILTINS_OBJS += test-match-trees.o
TEST_BUILTINS_OBJS += test-mergesort.o
TEST_BUILTINS_OBJS += test-mktemp.o
TEST_BUILTINS_OBJS += test-name-hash.o
+TEST_BUILTINS_OBJS += test-notes-external-config-reset.o
TEST_BUILTINS_OBJS += test-online-cpus.o
TEST_BUILTINS_OBJS += test-pack-deltas.o
TEST_BUILTINS_OBJS += test-pack-mtimes.o
@@ -1206,6 +1207,7 @@ LIB_OBJS += negotiator/default.o
LIB_OBJS += negotiator/noop.o
LIB_OBJS += negotiator/skipping.o
LIB_OBJS += notes-cache.o
+LIB_OBJS += notes-external.o
LIB_OBJS += notes-merge.o
LIB_OBJS += notes-utils.o
LIB_OBJS += notes.o
diff --git a/builtin/log.c b/builtin/log.c
index 8c0939dd42ad..bed4c1576f2d 100644
--- a/builtin/log.c
+++ b/builtin/log.c
@@ -1337,9 +1337,24 @@ static void get_notes_args(struct strvec *arg, struct rev_info *rev)
(rev->notes_opt.use_default_notes == -1 &&
!rev->notes_opt.extra_notes_refs.nr)) {
strvec_push(arg, "--notes");
- } else {
+ } else if (rev->notes_opt.extra_notes_refs.nr) {
for_each_string_list(&rev->notes_opt.extra_notes_refs, get_notes_refs, arg);
+ } else if (rev->notes_opt.use_external_notes <= 0) {
+ /*
+ * rev->show_notes can stay set after
+ * --external-notes --no-external-notes.
+ *
+ * Since range-diff's child log starts with
+ * --show-notes-by-default, explicitly suppress
+ * notes when no notes source remains.
+ */
+ strvec_push(arg, "--no-notes");
}
+
+ if (rev->notes_opt.use_external_notes > 0)
+ strvec_push(arg, "--external-notes");
+ else if (rev->notes_opt.use_external_notes == 0)
+ strvec_push(arg, "--no-external-notes");
}
static void generate_shortlog_cover_letter(struct shortlog *log,
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 60cbbfb4b7d1..5a0e7daac803 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -277,6 +277,7 @@ struct name_ref_data {
struct pretty_format {
struct pretty_print_context ctx;
struct userformat_want want;
+ struct external_notes_state *external_notes_state;
};
enum command_type {
@@ -525,9 +526,9 @@ static const char *get_format_rev(const struct commit *c,
if (format_ctx->want.notes) {
struct strbuf notebuf = STRBUF_INIT;
- format_display_notes(&c->object.oid, ¬ebuf,
- get_log_output_encoding(),
- format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT);
+ format_display_notes(c, ¬ebuf, get_log_output_encoding(),
+ format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT,
+ format_ctx->external_notes_state);
format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL);
}
@@ -879,6 +880,8 @@ int cmd_format_rev(int argc,
&ignore_show_notes,
n->string);
load_display_notes(&format_notes_opt);
+ format_pp.external_notes_state =
+ format_notes_opt.external_notes_state;
}
init_format_rev_command(&cmd, &format_pp);
diff --git a/builtin/range-diff.c b/builtin/range-diff.c
index e54c0f7fe156..41c27250404a 100644
--- a/builtin/range-diff.c
+++ b/builtin/range-diff.c
@@ -56,6 +56,8 @@ int cmd_range_diff(int argc,
OPT_PASSTHRU_ARGV(0, "notes", &log_arg,
N_("notes"), N_("passed to 'git log'"),
PARSE_OPT_OPTARG),
+ OPT_PASSTHRU_ARGV(0, "external-notes", &log_arg, NULL,
+ N_("passed to 'git log'"), PARSE_OPT_NOARG),
OPT_PASSTHRU_ARGV(0, "diff-merges", &diff_merges_arg,
N_("style"), N_("passed to 'git log'"), 0),
OPT_CALLBACK(0, "max-memory", &range_diff_opts.max_memory,
diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash
index a8e7c6ddbfb2..146444e65860 100644
--- a/contrib/completion/git-completion.bash
+++ b/contrib/completion/git-completion.bash
@@ -2023,7 +2023,7 @@ _git_fetch ()
__git_format_patch_extra_options="
--full-index --not --all --no-prefix --src-prefix=
- --dst-prefix= --notes
+ --dst-prefix= --notes --external-notes --no-external-notes
"
_git_format_patch ()
@@ -2215,7 +2215,7 @@ __git_log_common_options="
__git_log_gitk_options="
--dense --sparse --full-history
--simplify-merges --simplify-by-decoration
- --left-right --notes --no-notes
+ --left-right --notes --no-notes --external-notes --no-external-notes
"
# Options that go well for log and shortlog (not gitk)
__git_log_shortlog_options="
diff --git a/log-tree.c b/log-tree.c
index 4503a42dde6b..f37c8b14e9a1 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -856,9 +856,12 @@ void show_log(struct rev_info *opt)
if (opt->show_notes) {
struct strbuf notebuf = STRBUF_INIT;
bool raw = (opt->commit_format == CMIT_FMT_USERFORMAT);
+ const struct display_notes_opt *notes_opt = &opt->notes_opt;
+
+ format_display_notes(commit, ¬ebuf,
+ get_log_output_encoding(), raw,
+ notes_opt->external_notes_state);
- format_display_notes(&commit->object.oid, ¬ebuf,
- get_log_output_encoding(), raw);
ctx.notes_message = strbuf_detach(¬ebuf, NULL);
}
diff --git a/meson.build b/meson.build
index 052c81f2887b..83845f84fed0 100644
--- a/meson.build
+++ b/meson.build
@@ -397,6 +397,7 @@ libgit_sources = [
'notes-merge.c',
'notes-utils.c',
'notes.c',
+ 'notes-external.c',
'object-file-convert.c',
'object-file.c',
'object-name.c',
diff --git a/notes-external.c b/notes-external.c
new file mode 100644
index 000000000000..09da102a7901
--- /dev/null
+++ b/notes-external.c
@@ -0,0 +1,414 @@
+#include "git-compat-util.h"
+#include "gettext.h"
+#include "hex.h"
+#include "notes-external.h"
+#include "run-command.h"
+#include "sigchain.h"
+#include "strbuf.h"
+#include "trace.h"
+
+#define convert_ms_to_ns(ms) (uint64_t)(ms) * 1000000ULL
+#define convert_ns_to_ms(ns) (uint64_t)(ns) / 1000000ULL
+#define EXTERNAL_NOTES_DEFAULT_TIMEOUT_MS 100
+#define EXTERNAL_NOTES_READ_CHUNK_SIZE 16384 /* (16 * 1024) bytes */
+
+/* Configuration helpers. */
+
+static void init_external_notes_config(struct external_notes_config *config)
+{
+ if (!config)
+ return;
+
+ memset(config, 0, sizeof(*config));
+ config->read_timeout_ns =
+ convert_ms_to_ns(EXTERNAL_NOTES_DEFAULT_TIMEOUT_MS);
+}
+
+static void release_external_notes_config(struct external_notes_config *config)
+{
+ if (!config)
+ return;
+
+ FREE_AND_NULL(config->command);
+ FREE_AND_NULL(config->command_name_value);
+}
+
+struct external_notes_state *external_notes_new(void)
+{
+ struct external_notes_state *state = xcalloc(1, sizeof(*state));
+
+ init_external_notes_config(&state->config);
+ child_process_init(&state->process.process);
+ state->process.out_fd = -1;
+
+ return state;
+}
+
+void set_external_notes_command(struct external_notes_state *state,
+ const char *command)
+{
+ struct external_notes_config *config;
+
+ if (!state)
+ return;
+
+ config = &state->config;
+ FREE_AND_NULL(config->command);
+
+ if (command && *command)
+ config->command = xstrdup(command);
+}
+
+bool external_notes_command_configured(const struct external_notes_state *state)
+{
+ return state && state->config.command && !state->process.failed;
+}
+
+void external_notes_reset(struct external_notes_state *state)
+{
+ if (!state)
+ return;
+
+ if (state->process.started)
+ BUG("cannot reset external notes config while cmd is running");
+
+ release_external_notes_config(&state->config);
+ init_external_notes_config(&state->config);
+ state->process.failed = false;
+}
+
+void set_external_notes_command_name(struct external_notes_state *state,
+ const char *name)
+{
+ struct external_notes_config *config;
+
+ if (!state)
+ return;
+
+ config = &state->config;
+ FREE_AND_NULL(config->command_name_value);
+
+ if (name && *name)
+ config->command_name_value = xstrdup(name);
+}
+
+const char *external_notes_command_name(const struct external_notes_state *state)
+{
+ if (state && state->config.command_name_value)
+ return state->config.command_name_value;
+
+ return "external";
+}
+
+void set_external_notes_command_timeout_ms(struct external_notes_state *state,
+ int timeout_ms)
+{
+ if (!state)
+ return;
+
+ if (timeout_ms < 0)
+ BUG("negative notes.externalCommandTimeoutMs");
+
+ state->config.read_timeout_ns = convert_ms_to_ns(timeout_ms);
+}
+
+int external_notes_command_timeout_ms(const struct external_notes_state *state)
+{
+ if (!state)
+ return -1;
+
+ return (int)convert_ns_to_ms(state->config.read_timeout_ns);
+}
+
+void set_external_notes_for_grep(struct external_notes_state *state,
+ int enabled)
+{
+ if (!state)
+ return;
+
+ state->config.for_grep = (bool)enabled;
+}
+
+bool external_notes_for_grep_enabled(const struct external_notes_state *state)
+{
+ return state && state->config.for_grep;
+}
+
+/* Process management helpers. */
+
+static void mute_routine(const char *msg UNUSED, va_list params UNUSED)
+{
+ /* do nothing */
+}
+
+static void close_external_notes_pipes(struct external_notes_process *state)
+{
+ struct child_process *process;
+
+ if (!state)
+ return;
+
+ process = &state->process;
+
+ sigchain_push(SIGPIPE, SIG_IGN);
+
+ if (state->in) {
+ fclose(state->in);
+ state->in = NULL;
+ } else {
+ close(process->in);
+ }
+
+ if (state->out_fd >= 0) {
+ close(state->out_fd);
+ state->out_fd = -1;
+ } else {
+ close(process->out);
+ }
+
+ sigchain_pop(SIGPIPE);
+}
+
+/* We set this as callback later, so can't have void argument. */
+static void cleanup_external_notes_process(struct child_process *process)
+{
+ report_fn old_error = NULL;
+ struct external_notes_process *state;
+
+ if (!process)
+ return;
+
+ state = container_of(process, struct external_notes_process, process);
+
+ kill(process->pid, SIGTERM);
+ old_error = get_error_routine();
+ set_error_routine(mute_routine);
+
+ close_external_notes_pipes(state);
+ finish_command(process);
+
+ if (old_error)
+ set_error_routine(old_error);
+
+ state->started = false;
+}
+
+static void stop_external_notes_process(struct external_notes_process *state)
+{
+ if (!state)
+ return;
+
+ if (!state->started)
+ return;
+
+ state->process.clean_on_exit = 0;
+ cleanup_external_notes_process(&state->process);
+ child_process_init(&state->process);
+ state->out_fd = -1;
+}
+
+static int fail_external_notes_command(struct external_notes_state *state)
+{
+ const struct external_notes_config *config;
+ struct external_notes_process *process;
+
+ if (!state)
+ return -1;
+
+ config = &state->config;
+ process = &state->process;
+ if (!process->failed)
+ warning(_("notes.externalCommand failed: %s"),
+ config->command);
+
+ process->failed = true;
+ stop_external_notes_process(process);
+ return -1;
+}
+
+static int start_external_notes_command(struct external_notes_state *state)
+{
+ const struct external_notes_config *config;
+ struct external_notes_process *process;
+ struct child_process *cmd;
+
+ if (!state)
+ return -1;
+
+ config = &state->config;
+ process = &state->process;
+ cmd = &process->process;
+
+ if (process->started)
+ return 0;
+
+ if (!config->command || process->failed)
+ return -1;
+
+ child_process_init(cmd);
+ strvec_push(&cmd->args, config->command);
+ cmd->use_shell = 1;
+ cmd->in = -1;
+ cmd->out = -1;
+ cmd->clean_on_exit = 1;
+ cmd->clean_on_exit_handler = cleanup_external_notes_process;
+ cmd->trace2_child_class = "notes-external";
+
+ if (start_command(cmd))
+ return fail_external_notes_command(state);
+
+ process->in = xfdopen(cmd->in, "wb");
+ process->out_fd = cmd->out;
+ process->started = true;
+
+ return 0;
+}
+
+void external_notes_free(struct external_notes_state *state)
+{
+ if (!state)
+ return;
+
+ stop_external_notes_process(&state->process);
+ release_external_notes_config(&state->config);
+ free(state);
+}
+
+/* Command parser. Essentially the main() function of this file. */
+int format_external_note(struct external_notes_state *state,
+ const struct object_id *object_oid,
+ struct strbuf *note_buf)
+{
+ struct strbuf status = STRBUF_INIT;
+ char commit_id_hex_str[GIT_MAX_HEXSZ + 1];
+ const char *arg;
+ char *end;
+ char ch;
+ unsigned long len;
+ uint64_t deadline_ns;
+ bool input_fail;
+ int ret = 0;
+ const struct external_notes_config *config;
+ struct external_notes_process *process;
+
+ if (!state)
+ return -1;
+
+ /* Exit early if starting the command fails. */
+ if (start_external_notes_command(state) != 0)
+ return -1;
+
+ config = &state->config;
+ process = &state->process;
+
+ /* Fetch the commit ID hex. */
+ oid_to_hex_r(commit_id_hex_str, object_oid);
+
+ /* Pass the input to the external command. */
+ sigchain_push(SIGPIPE, SIG_IGN);
+ input_fail = fprintf(process->in, "%s\n", commit_id_hex_str) < 0
+ || fflush(process->in) != 0;
+ sigchain_pop(SIGPIPE);
+
+ if (input_fail)
+ goto out_fail;
+
+ if (config->read_timeout_ns == 0)
+ deadline_ns = 0;
+ else
+ deadline_ns = getnanotime() + config->read_timeout_ns;
+
+ /**
+ * The output for each commit is either of the two:
+ * "{commit id} missing\n"
+ * "{commit id} ok {num_bytes}\n{str_of_num_bytes}\n"
+ *
+ * We can have "\r\n" instead of "\n" due to Windows.
+ */
+
+ /* Read the first line with its delimiter. */
+ if (strbuf_getwholeline_fd_deadline(&status, process->out_fd, '\n',
+ deadline_ns) == EOF)
+ goto out_fail;
+
+ /* Reject EOF-terminated partial lines. */
+ if (!status.len || status.buf[status.len - 1] != '\n')
+ goto out_fail;
+
+ /**
+ * Strip LF and then optional CR so both LF and CRLF protocol lines
+ * are accepted.
+ */
+ strbuf_setlen(&status, status.len - 1);
+ strbuf_strip_suffix(&status, "\r");
+
+ /* Check if line starts with the commit ID. */
+ if (!skip_prefix(status.buf, commit_id_hex_str, &arg))
+ goto out_fail;
+
+ if (*arg++ != ' ') /* After commit ID there should be a space. */
+ goto out_fail;
+
+ if (strcmp(arg, "missing") == 0) /* No note available. */
+ goto out_success; /* Ending newline is already ensured. */
+
+ if (!skip_prefix(arg, "ok ", &arg)) /* Neither missing nor ok. */
+ goto out_fail;
+
+ /* We are in "ok" case. */
+
+ /* The next thing is length of the note. It must be unsigned digits. */
+ if (!isdigit(*arg))
+ goto out_fail;
+
+ /* Get the length of note. */
+ errno = 0;
+ len = strtoul(arg, &end, 10);
+ if (errno != 0 || *end != '\0' || end == arg)
+ goto out_fail;
+
+ /* Ending newline is already ensured. */
+
+ /* Read the trailing note in bounded-chunks. */
+ while (note_buf->len < len) {
+ ssize_t got;
+ size_t remaining = len - note_buf->len;
+ size_t want = remaining < EXTERNAL_NOTES_READ_CHUNK_SIZE ?
+ remaining : EXTERNAL_NOTES_READ_CHUNK_SIZE;
+
+ strbuf_grow(note_buf, want);
+
+ got = read_in_full_deadline(process->out_fd,
+ note_buf->buf + note_buf->len,
+ want, deadline_ns);
+ if (got < 0 || (size_t)got != want)
+ goto out_fail;
+
+ strbuf_setlen(note_buf, note_buf->len + (size_t)got);
+ }
+
+ /* Ensure the ending newline (LF/CRLF) after the note. */
+ if (xread_deadline(process->out_fd, &ch, 1, deadline_ns) != 1)
+ goto out_fail;
+
+ if (ch != '\n') { /* Not a LF. */
+ if (ch != '\r') /* Not a CRLF. */
+ goto out_fail;
+
+ /* We have '\r', let's read the next char. */
+ if (xread_deadline(process->out_fd, &ch, 1,
+ deadline_ns) != 1)
+ goto out_fail;
+
+ if (ch != '\n') /* Not a CRLF. */
+ goto out_fail;
+ }
+
+ goto out_success;
+
+out_fail:
+ ret = fail_external_notes_command(state);
+out_success:
+ strbuf_release(&status);
+ return ret;
+}
diff --git a/notes-external.h b/notes-external.h
new file mode 100644
index 000000000000..1b5c2d3919a2
--- /dev/null
+++ b/notes-external.h
@@ -0,0 +1,53 @@
+#ifndef NOTES_EXTERNAL_H
+#define NOTES_EXTERNAL_H
+
+#include "run-command.h"
+
+struct object_id;
+struct strbuf;
+
+struct external_notes_config {
+ char *command;
+ char *command_name_value;
+ uint64_t read_timeout_ns;
+ bool for_grep;
+};
+
+struct external_notes_process {
+ struct child_process process;
+ FILE *in;
+ int out_fd;
+ bool started;
+ bool failed;
+};
+
+struct external_notes_state {
+ struct external_notes_config config;
+ struct external_notes_process process;
+};
+
+struct external_notes_state *external_notes_new(void);
+void external_notes_free(struct external_notes_state *state);
+void external_notes_reset(struct external_notes_state *state);
+
+void set_external_notes_command(struct external_notes_state *state,
+ const char *command);
+bool external_notes_command_configured(const struct external_notes_state *state);
+
+void set_external_notes_command_name(struct external_notes_state *state,
+ const char *name);
+const char *external_notes_command_name(const struct external_notes_state *state);
+
+void set_external_notes_command_timeout_ms(struct external_notes_state *state,
+ int timeout_ms);
+int external_notes_command_timeout_ms(const struct external_notes_state *state);
+
+void set_external_notes_for_grep(struct external_notes_state *state,
+ int enabled);
+bool external_notes_for_grep_enabled(const struct external_notes_state *state);
+
+int format_external_note(struct external_notes_state *state,
+ const struct object_id *object_oid,
+ struct strbuf *out);
+
+#endif /* NOTES_EXTERNAL_H */
diff --git a/notes.c b/notes.c
index 201f1df3dc29..624d4aba223d 100644
--- a/notes.c
+++ b/notes.c
@@ -3,9 +3,12 @@
#include "git-compat-util.h"
#include "config.h"
+#include "commit.h"
#include "environment.h"
+#include "gettext.h"
#include "hex.h"
#include "notes.h"
+#include "notes-external.h"
#include "object-file.h"
#include "object-name.h"
#include "odb.h"
@@ -983,18 +986,59 @@ void string_list_add_refs_from_colon_sep(struct string_list *list,
free(globs_copy);
}
+struct notes_display_config_data {
+ int load_refs;
+ int load_command;
+ struct external_notes_state *external_notes_state;
+};
+
static int notes_display_config(const char *k, const char *v,
- const struct config_context *ctx UNUSED,
+ const struct config_context *ctx,
void *cb)
{
- int *load_refs = cb;
+ struct notes_display_config_data *data = cb;
- if (*load_refs && !strcmp(k, "notes.displayref")) {
+ if (data->load_refs && !strcmp(k, "notes.displayref")) {
if (!v)
return config_error_nonbool(k);
string_list_add_refs_by_glob(&display_notes_refs, v);
}
+ if (data->load_command && !strcmp(k, "notes.externalcommand")) {
+ if (!v)
+ return config_error_nonbool(k);
+
+ set_external_notes_command(data->external_notes_state, v);
+ }
+
+ if (data->load_command && !strcmp(k, "notes.externalcommandname")) {
+ if (!v)
+ return config_error_nonbool(k);
+
+ if (strchr(v, '\n') || strchr(v, '\r'))
+ return error(_("notes.externalCommandName must not contain a newline"));
+
+ set_external_notes_command_name(data->external_notes_state, v);
+ }
+
+ if (data->load_command && !strcmp(k, "notes.externalcommandtimeoutms")) {
+ int timeout_ms;
+
+ if (!v)
+ return config_error_nonbool(k);
+
+ timeout_ms = git_config_int(k, v, ctx->kvi);
+ if (timeout_ms < 0)
+ return error(_("notes.externalCommandTimeoutMs must be non-negative"));
+
+ set_external_notes_command_timeout_ms(data->external_notes_state,
+ timeout_ms);
+ }
+
+ if (data->load_command && !strcmp(k, "notes.externalcommandforgrep"))
+ set_external_notes_for_grep(data->external_notes_state,
+ git_config_bool(k, v));
+
return 0;
}
@@ -1075,17 +1119,21 @@ void init_display_notes(struct display_notes_opt *opt)
{
memset(opt, 0, sizeof(*opt));
opt->use_default_notes = -1;
+ opt->use_external_notes = -1;
string_list_init_dup(&opt->extra_notes_refs);
}
void release_display_notes(struct display_notes_opt *opt)
{
string_list_clear(&opt->extra_notes_refs, 0);
+ external_notes_free(opt->external_notes_state);
+ opt->external_notes_state = NULL;
}
void enable_default_display_notes(struct display_notes_opt *opt, int *show_notes)
{
opt->use_default_notes = 1;
+ opt->default_notes_suppressed_by_external = 0;
*show_notes = 1;
}
@@ -1102,31 +1150,96 @@ void enable_ref_display_notes(struct display_notes_opt *opt, int *show_notes,
void disable_display_notes(struct display_notes_opt *opt, int *show_notes)
{
opt->use_default_notes = -1;
+ opt->use_external_notes = -1;
+ opt->default_notes_suppressed_by_external = 0;
string_list_clear(&opt->extra_notes_refs, 0);
*show_notes = 0;
}
+/*
+ * Resolve the default-notes tri-state in one place. Callers must not test
+ * use_default_notes directly unless they specifically need the unresolved
+ * command-line state.
+ */
+static bool display_notes_use_default(const struct display_notes_opt *opt)
+{
+ /* Options aren't specified, default to true. */
+ if (!opt)
+ return true;
+
+ /* Explicitly enabled. */
+ if (opt->use_default_notes > 0)
+ return true;
+
+ /* Undefined and no explicit notes-ref specified, default to true. */
+ if (opt->use_default_notes == -1 && !opt->extra_notes_refs.nr)
+ return true;
+
+ return false;
+}
+
+/*
+ * Resolve the external-notes tri-state. The unset value follows the resolved
+ * default-notes decision, which means "git log" runs the helper by default
+ * but "git log --notes=<ref>" does not.
+ */
+static bool display_notes_use_external(const struct display_notes_opt *opt)
+{
+ /* Options aren't specified, default to false. */
+ if (!opt)
+ return false;
+
+ /* Explicitly enabled. */
+ if (opt->use_external_notes > 0)
+ return true;
+
+ /* Undefined and to use default notes set, default to true. */
+ if (opt->use_external_notes < 0 && display_notes_use_default(opt))
+ return true;
+
+ return false;
+}
+
void load_display_notes(struct display_notes_opt *opt)
{
char *display_ref_env;
- int load_config_refs = 0;
+ struct notes_display_config_data config = { 0, 0 };
+ struct notes_display_config_data protected_config = { 0, 0 };
+ bool use_default_notes = display_notes_use_default(opt);
+ bool use_external_notes = display_notes_use_external(opt);
+
display_notes_refs.strdup_strings = 1;
+ if (use_external_notes && opt->external_notes_state) {
+ external_notes_reset(opt->external_notes_state);
+ } else if (opt) {
+ external_notes_free(opt->external_notes_state);
+ opt->external_notes_state = NULL;
+ }
+
assert(!display_notes_trees);
- if (!opt || opt->use_default_notes > 0 ||
- (opt->use_default_notes == -1 && !opt->extra_notes_refs.nr)) {
+ if (use_default_notes) {
string_list_append_nodup(&display_notes_refs, default_notes_ref(the_repository));
display_ref_env = getenv(GIT_NOTES_DISPLAY_REF_ENVIRONMENT);
if (display_ref_env) {
string_list_add_refs_from_colon_sep(&display_notes_refs,
display_ref_env);
- load_config_refs = 0;
+ config.load_refs = 0;
} else
- load_config_refs = 1;
+ config.load_refs = 1;
}
- repo_config(the_repository, notes_display_config, &load_config_refs);
+ if (use_external_notes) {
+ if (!opt->external_notes_state)
+ opt->external_notes_state = external_notes_new();
+
+ protected_config.load_command = 1;
+ protected_config.external_notes_state = opt->external_notes_state;
+ }
+
+ repo_config(the_repository, notes_display_config, &config);
+ git_protected_config(notes_display_config, &protected_config);
if (opt) {
struct string_list_item *item;
@@ -1266,47 +1379,31 @@ void free_notes(struct notes_tree *t)
}
/*
- * Fill the given strbuf with the notes associated with the given object.
+ * Append one already-loaded note message to the given strbuf.
*
- * If the given notes_tree structure is not initialized, it will be auto-
- * initialized to the default value (see documentation for init_notes() above).
- * If the given notes_tree is NULL, the internal/default notes_tree will be
- * used instead.
+ * Notes read from refs and notes obtained from notes.externalCommand both use
+ * this helper so they share the same encoding, header, and indentation rules.
*
* (raw == true) gives the %N userformat; otherwise, the note message is given
* for human consumption.
*/
-static void format_note(struct notes_tree *t, const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, bool raw)
+static void format_note_data(const char *ref, const char *msg, size_t msglen,
+ struct strbuf *sb, const char *output_encoding,
+ bool raw, bool literal_ref)
{
static const char utf8[] = "utf-8";
- const struct object_id *oid;
- char *msg, *msg_p;
- unsigned long linelen, msglen;
- enum object_type type;
-
- if (!t)
- t = &default_notes_tree;
- if (!t->initialized)
- init_notes(t, NULL, NULL, 0);
-
- oid = get_note(t, object_oid);
- if (!oid)
- return;
-
- if (!(msg = odb_read_object(the_repository->objects, oid, &type, &msglen)) ||
- type != OBJ_BLOB) {
- free(msg);
- return;
- }
+ char *reencoded = NULL;
+ const char *msg_p, *msg_end;
+ /* Convert the note text from UTF-8 to the requested output encoding. */
if (output_encoding && *output_encoding &&
!is_encoding_utf8(output_encoding)) {
- char *reencoded = reencode_string(msg, output_encoding, utf8);
+ size_t reencoded_len;
+ reencoded = reencode_string_len(msg, msglen, output_encoding,
+ utf8, &reencoded_len);
if (reencoded) {
- free(msg);
msg = reencoded;
- msglen = strlen(msg);
+ msglen = reencoded_len;
}
}
@@ -1314,37 +1411,106 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid
if (msglen && msg[msglen - 1] == '\n')
msglen--;
+ /* Raw mode is the %N userformat, so it omits the "Notes" header. */
if (!raw) {
- const char *ref = t->ref;
- if (!ref || !strcmp(ref, GIT_NOTES_DEFAULT_REF)) {
+ if (!ref)
strbuf_addstr(sb, "\nNotes:\n");
- } else {
- skip_prefix(ref, "refs/", &ref);
- skip_prefix(ref, "notes/", &ref);
+ else if (!literal_ref && !strcmp(ref, GIT_NOTES_DEFAULT_REF))
+ strbuf_addstr(sb, "\nNotes:\n");
+ else {
+ if (!literal_ref) {
+ skip_prefix(ref, "refs/", &ref);
+ skip_prefix(ref, "notes/", &ref);
+ }
strbuf_addf(sb, "\nNotes (%s):\n", ref);
}
}
- for (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) {
- linelen = strchrnul(msg_p, '\n') - msg_p;
+ msg_end = msg + msglen;
+ for (msg_p = msg; msg_p < msg_end; ) {
+ const char *eol = memchr(msg_p, '\n', msg_end - msg_p);
+ size_t linelen = eol ? eol - msg_p : msg_end - msg_p;
+ /* Human output indents note body lines under the header. */
if (!raw)
strbuf_addstr(sb, " ");
+
strbuf_add(sb, msg_p, linelen);
strbuf_addch(sb, '\n');
+
+ msg_p += linelen;
+ if (msg_p < msg_end)
+ msg_p++;
+ }
+
+ free(reencoded);
+}
+
+/*
+ * Fill the given strbuf with the notes associated with the given object.
+ *
+ * If the given notes_tree structure is not initialized, it will be auto-
+ * initialized to the default value (see documentation for init_notes() above).
+ * If the given notes_tree is NULL, the internal/default notes_tree will be
+ * used instead.
+ */
+static void format_note_from_tree(struct notes_tree *t,
+ const struct object_id *object_oid,
+ struct strbuf *sb,
+ const char *output_encoding, bool raw)
+{
+ const struct object_id *oid;
+ char *msg;
+ unsigned long msglen;
+ enum object_type type;
+
+ if (!t)
+ t = &default_notes_tree;
+ if (!t->initialized)
+ init_notes(t, NULL, NULL, 0);
+
+ oid = get_note(t, object_oid);
+ if (!oid)
+ return;
+
+ if (!(msg = odb_read_object(the_repository->objects, oid, &type, &msglen)) ||
+ type != OBJ_BLOB) {
+ free(msg);
+ return;
}
+ format_note_data(t->ref, msg, msglen, sb, output_encoding, raw, false);
+
free(msg);
}
-void format_display_notes(const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, bool raw)
+void format_display_notes(const struct commit *commit,
+ struct strbuf *sb, const char *output_encoding,
+ bool raw,
+ struct external_notes_state *external_state)
{
int i;
+ const struct object_id *commit_oid = &commit->object.oid;
+
assert(display_notes_trees);
for (i = 0; display_notes_trees[i]; i++)
- format_note(display_notes_trees[i], object_oid, sb,
- output_encoding, raw);
+ format_note_from_tree(display_notes_trees[i], commit_oid, sb,
+ output_encoding, raw);
+
+ if (external_notes_command_configured(external_state)) {
+ struct strbuf out = STRBUF_INIT;
+
+ if (format_external_note(external_state, commit_oid, &out) == 0
+ && out.len) {
+ const char *label =
+ external_notes_command_name(external_state);
+
+ format_note_data(label, out.buf, out.len, sb,
+ output_encoding, raw, true);
+ }
+
+ strbuf_release(&out);
+ }
}
int copy_note(struct notes_tree *t,
diff --git a/notes.h b/notes.h
index f6410b31e1c9..5ac6a7e01dfb 100644
--- a/notes.h
+++ b/notes.h
@@ -6,6 +6,8 @@
struct object_id;
struct repository;
struct strbuf;
+struct commit;
+struct external_notes_state;
/*
* Function type for combining two notes annotating the same object.
@@ -264,11 +266,31 @@ struct display_notes_opt {
*/
int use_default_notes;
+ /*
+ * Less than `0` is "unset", which means external notes are shown iff
+ * the default notes are shown. Otherwise, treat it like a boolean.
+ */
+ int use_external_notes;
+
+ /*
+ * Tracks the synthetic "default notes off" state introduced by
+ * `--external-notes`, so a later deprecated `--show-notes=<ref>`
+ * can still preserve its historical additive behavior without
+ * overriding an explicit `--no-standard-notes`.
+ */
+ int default_notes_suppressed_by_external;
+
/*
* A list of globs (in the same style as notes.displayRef) where
* notes should be loaded from.
*/
struct string_list extra_notes_refs;
+
+ /*
+ * State for notes.externalCommand. This is initialized lazily by
+ * load_display_notes() when external notes may be used.
+ */
+ struct external_notes_state *external_notes_state;
};
/*
@@ -304,16 +326,21 @@ void disable_display_notes(struct display_notes_opt *opt, int *show_notes);
void load_display_notes(struct display_notes_opt *opt);
/*
- * Append notes for the given 'object_sha1' from all trees set up by
+ * Append notes for the given commit from all trees set up by
* load_display_notes() to 'sb'.
*
* If 'raw' is false the note will be indented by 4 places and
* a 'Notes (refname):' header added.
*
+ * If 'external_state' is not NULL, notes.externalCommand will be used to
+ * append the note from an external source.
+ *
* You *must* call load_display_notes() before using this function.
*/
-void format_display_notes(const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, bool raw);
+void format_display_notes(const struct commit *commit,
+ struct strbuf *sb, const char *output_encoding,
+ bool raw,
+ struct external_notes_state *external_state);
/*
* Load the notes tree from each ref listed in 'refs'. The output is
diff --git a/revision.c b/revision.c
index cd9fcefa0a88..f9581fa82f95 100644
--- a/revision.c
+++ b/revision.c
@@ -6,6 +6,7 @@
#include "environment.h"
#include "gettext.h"
#include "hex.h"
+#include "notes-external.h"
#include "object-name.h"
#include "object-file.h"
#include "odb.h"
@@ -2583,18 +2584,40 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
} else if (skip_prefix(arg, "--show-notes=", &optarg) ||
skip_prefix(arg, "--notes=", &optarg)) {
if (starts_with(arg, "--show-notes=") &&
- revs->notes_opt.use_default_notes < 0)
+ (revs->notes_opt.use_default_notes < 0 ||
+ revs->notes_opt.default_notes_suppressed_by_external)) {
revs->notes_opt.use_default_notes = 1;
+ revs->notes_opt.default_notes_suppressed_by_external = 0;
+ }
enable_ref_display_notes(&revs->notes_opt, &revs->show_notes, optarg);
revs->show_notes_given = 1;
} else if (!strcmp(arg, "--no-notes")) {
disable_display_notes(&revs->notes_opt, &revs->show_notes);
revs->show_notes_given = 1;
+ } else if (!strcmp(arg, "--external-notes")) {
+ revs->notes_opt.use_external_notes = 1;
+ revs->show_notes = 1;
+ revs->show_notes_given = 1;
+ /*
+ * `--external-notes` names a note source on its own. If the
+ * default notes ref is still undecided, settle it to "off" so
+ * this option does not also trigger the "no explicit notes
+ * refs" fallback. A later use of `--notes` or the deprecated
+ * `--show-notes=<ref>` can still turn the default ref on.
+ */
+ if (revs->notes_opt.use_default_notes < 0) {
+ revs->notes_opt.use_default_notes = 0;
+ revs->notes_opt.default_notes_suppressed_by_external = 1;
+ }
+ } else if (!strcmp(arg, "--no-external-notes")) {
+ revs->notes_opt.use_external_notes = 0;
} else if (!strcmp(arg, "--standard-notes")) {
revs->show_notes_given = 1;
revs->notes_opt.use_default_notes = 1;
+ revs->notes_opt.default_notes_suppressed_by_external = 0;
} else if (!strcmp(arg, "--no-standard-notes")) {
revs->notes_opt.use_default_notes = 0;
+ revs->notes_opt.default_notes_suppressed_by_external = 0;
} else if (!strcmp(arg, "--oneline")) {
revs->verbose_header = 1;
get_commit_format("oneline", revs);
@@ -4105,9 +4128,18 @@ static int commit_match(struct commit *commit, struct rev_info *opt)
/* Append "fake" message parts as needed */
if (opt->show_notes) {
+ const struct display_notes_opt *notes_opt = &opt->notes_opt;
+ struct external_notes_state *external_notes_state =
+ notes_opt->external_notes_state;
+
if (!buf.len)
strbuf_addstr(&buf, message);
- format_display_notes(&commit->object.oid, &buf, encoding, true);
+
+ if (!external_notes_for_grep_enabled(external_notes_state))
+ external_notes_state = NULL;
+
+ format_display_notes(commit, &buf, encoding, true,
+ external_notes_state);
}
/*
diff --git a/t/helper/meson.build b/t/helper/meson.build
index 3235f10ab8aa..15b6198c19fe 100644
--- a/t/helper/meson.build
+++ b/t/helper/meson.build
@@ -35,6 +35,7 @@ test_tool_sources = [
'test-mergesort.c',
'test-mktemp.c',
'test-name-hash.c',
+ 'test-notes-external-config-reset.c',
'test-online-cpus.c',
'test-pack-deltas.c',
'test-pack-mtimes.c',
diff --git a/t/helper/test-external-notes b/t/helper/test-external-notes
new file mode 100755
index 000000000000..5e9dde3977ab
--- /dev/null
+++ b/t/helper/test-external-notes
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+prefix=${TEST_EXTERNAL_NOTES_PREFIX:-external-notes}
+response=${TEST_EXTERNAL_NOTES_RESPONSE:-ok}
+line_ending=${TEST_EXTERNAL_NOTES_LINE_ENDING:-lf}
+exit_after_response=${TEST_EXTERNAL_NOTES_EXIT_AFTER_RESPONSE:-}
+exit_delay=${TEST_EXTERNAL_NOTES_EXIT_DELAY:-}
+delay=${TEST_EXTERNAL_NOTES_DELAY:-}
+char_delay=${TEST_EXTERNAL_NOTES_CHAR_DELAY:-}
+ignore_term=${TEST_EXTERNAL_NOTES_IGNORE_TERM:-}
+
+newline='\n'
+case "$line_ending" in
+crlf)
+ newline='\r\n'
+ ;;
+none)
+ newline=
+ ;;
+esac
+
+echo start >>"$prefix-starts"
+
+test "$ignore_term" = true && trap '' TERM
+
+emit_output() {
+ if test -n "$char_delay"
+ then
+ LC_ALL=C
+ payload=$(printf "$@"; printf .)
+ payload=${payload%.}
+
+ while test -n "$payload"
+ do
+ char=${payload%"${payload#?}"}
+ printf '%s' "$char" || return 1
+ payload=${payload#?}
+ sleep "$char_delay" || return 1
+ done
+ else
+ printf "$@"
+ fi
+}
+
+while IFS= read -r commit; do
+ if test "${TEST_EXTERNAL_NOTES_BODY+x}" = x
+ then
+ note=$TEST_EXTERNAL_NOTES_BODY
+ else
+ note=$commit
+ fi
+ printf "%s\n" "$commit" >>"$prefix-requests"
+ test -z "$delay" || sleep "$delay"
+ if test "$response" = missing
+ then
+ emit_output "%s missing%b" "$commit" "$newline"
+ else
+ emit_output "%s ok %d%b%s%b" \
+ "$commit" "${#note}" "$newline" "$note" "$newline"
+ fi
+ test "$exit_after_response" = true && break
+done
+
+test -z "$exit_delay" || sleep "$exit_delay"
diff --git a/t/helper/test-notes-external-config-reset.c b/t/helper/test-notes-external-config-reset.c
new file mode 100644
index 000000000000..a64d03346fb9
--- /dev/null
+++ b/t/helper/test-notes-external-config-reset.c
@@ -0,0 +1,24 @@
+#include "test-tool.h"
+#include "notes-external.h"
+
+int cmd__notes_external_config_reset(int argc, const char **argv UNUSED)
+{
+ struct external_notes_state *state;
+
+ if (argc != 1)
+ die("usage: test-tool notes-external-config-reset");
+
+ state = external_notes_new();
+ set_external_notes_command(state, "helper");
+ set_external_notes_command_name(state, "label");
+ set_external_notes_command_timeout_ms(state, 250);
+ set_external_notes_for_grep(state, 1);
+ external_notes_reset(state);
+
+ printf("configured=%d\n", external_notes_command_configured(state));
+ printf("name=%s\n", external_notes_command_name(state));
+ printf("timeout_ms=%d\n", external_notes_command_timeout_ms(state));
+ printf("grep=%d\n", external_notes_for_grep_enabled(state));
+ external_notes_free(state);
+ return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index b71a22b43bbc..b4de5a2f5c06 100644
--- a/t/helper/test-tool.c
+++ b/t/helper/test-tool.c
@@ -45,6 +45,7 @@ static struct test_cmd cmds[] = {
{ "mergesort", cmd__mergesort },
{ "mktemp", cmd__mktemp },
{ "name-hash", cmd__name_hash },
+ { "notes-external-config-reset", cmd__notes_external_config_reset },
{ "online-cpus", cmd__online_cpus },
{ "pack-deltas", cmd__pack_deltas },
{ "pack-mtimes", cmd__pack_mtimes },
diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h
index f2885b33d58a..e74d4d934b14 100644
--- a/t/helper/test-tool.h
+++ b/t/helper/test-tool.h
@@ -38,6 +38,7 @@ int cmd__match_trees(int argc, const char **argv);
int cmd__mergesort(int argc, const char **argv);
int cmd__mktemp(int argc, const char **argv);
int cmd__name_hash(int argc, const char **argv);
+int cmd__notes_external_config_reset(int argc, const char **argv);
int cmd__online_cpus(int argc, const char **argv);
int cmd__pack_deltas(int argc, const char **argv);
int cmd__pack_mtimes(int argc, const char **argv);
diff --git a/t/lib-notes.sh b/t/lib-notes.sh
new file mode 100644
index 000000000000..07422540d58f
--- /dev/null
+++ b/t/lib-notes.sh
@@ -0,0 +1,19 @@
+# Helpers for scripts testing notes behavior.
+
+# notes.externalCommand is run through a shell, so quote the path.
+external_notes_command=$(
+ printf "%s\n" "$TEST_DIRECTORY/helper/test-external-notes" |
+ sed "s/'/'\\\\''/g; s/^/'/; s/$/'/"
+)
+
+# The helper above is a shell script. Few Windows CI tests (3 out of 10
+# in matrix) are spending more than the production default timeout just
+# starting the shell and exchanging the first response, so tests that
+# are not about timeout behavior fail. So let us opt into a wider 1s
+# deadline for Windows instead of 100ms.
+external_notes_command_timeout_config=
+if test_have_prereq MINGW
+then
+ _timeout_config="notes.externalCommandTimeoutMs=1000"
+ external_notes_command_timeout_config="-c $_timeout_config"
+fi
diff --git a/t/t3206-range-diff.sh b/t/t3206-range-diff.sh
index 1e812df806bb..96adeb9bc4fe 100755
--- a/t/t3206-range-diff.sh
+++ b/t/t3206-range-diff.sh
@@ -6,6 +6,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-notes.sh
# Note that because of the range-diff's heuristics, test_commit does more
# harm than good. We need some real history.
@@ -690,6 +691,37 @@ test_expect_success 'range-diff with --notes=custom does not show default notes'
grep "## Notes (custom) ##" actual
'
+test_expect_success 'range-diff with --external-notes' '
+ topic_oid=$(git rev-parse topic) &&
+ unmodified_oid=$(git rev-parse unmodified) &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ range-diff --no-color --external-notes \
+ main..topic main..unmodified >actual &&
+ test_grep "## Notes (external) ##" actual &&
+ test_grep "^ - $topic_oid$" actual &&
+ test_grep "^ + $unmodified_oid$" actual &&
+ ! grep "## Notes ##" actual
+'
+
+test_expect_success 'range-diff with disabled external notes' '
+ test_when_finished "git notes remove topic unmodified || :" &&
+ git notes add -m "topic note" topic &&
+ git notes add -m "unmodified note" unmodified &&
+ TEST_EXTERNAL_NOTES_PREFIX=range-diff-external-notes \
+ git -c notes.externalCommand="$external_notes_command" \
+ range-diff --no-color --external-notes --no-external-notes \
+ main..topic main..unmodified >actual &&
+ cat >expect <<-EOF &&
+ 1: $(test_oid t1) = 1: $(test_oid u1) s/5/A/
+ 2: $(test_oid t2) = 2: $(test_oid u2) s/4/A/
+ 3: $(test_oid t3) = 3: $(test_oid u3) s/11/B/
+ 4: $(test_oid t4) = 4: $(test_oid u4) s/12/B/
+ EOF
+ test_cmp expect actual &&
+ test_path_is_missing range-diff-external-notes-starts
+'
+
test_expect_success 'format-patch --range-diff does not compare notes by default' '
test_when_finished "git notes remove topic unmodified || :" &&
git notes add -m "topic note" topic &&
@@ -780,6 +812,42 @@ test_expect_success 'format-patch --range-diff with --notes' '
test_cmp expect actual
'
+test_expect_success 'format-patch --range-diff with --external-notes' '
+ topic_oid=$(git rev-parse topic) &&
+ unmodified_oid=$(git rev-parse unmodified) &&
+ test_when_finished "rm -f 000?-*" &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ format-patch --external-notes --cover-letter --range-diff=$prev \
+ main..unmodified >actual &&
+ test_line_count = 5 actual &&
+ test_grep "^Range-diff:$" 0000-* &&
+ test_grep "## Notes (external) ##" 0000-* &&
+ test_grep "^ - $topic_oid$" 0000-* &&
+ test_grep "^ + $unmodified_oid$" 0000-* &&
+ ! grep "## Notes ##" 0000-*
+'
+
+test_expect_success 'format-patch --range-diff with disabled external notes' '
+ test_when_finished "git notes remove topic unmodified || :" &&
+ git notes add -m "topic note" topic &&
+ git notes add -m "unmodified note" unmodified &&
+ test_when_finished "rm -f 000?-*" &&
+ TEST_EXTERNAL_NOTES_PREFIX=range-diff-external-notes \
+ git -c notes.externalCommand="$external_notes_command" \
+ format-patch --external-notes --no-external-notes \
+ --cover-letter --range-diff=$prev main..unmodified >actual &&
+ test_line_count = 5 actual &&
+ test_grep "^Range-diff:$" 0000-* &&
+ grep "= 1: .* s/5/A" 0000-* &&
+ grep "= 2: .* s/4/A" 0000-* &&
+ grep "= 3: .* s/11/B" 0000-* &&
+ grep "= 4: .* s/12/B" 0000-* &&
+ ! grep "Notes" 0000-* &&
+ ! grep "note" 0000-* &&
+ test_path_is_missing range-diff-external-notes-starts
+'
+
test_expect_success 'format-patch --range-diff with format.notes config' '
test_when_finished "git notes remove topic unmodified || :" &&
git notes add -m "topic note" topic &&
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index 27439010dfbc..7fd82767c1f1 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -6,6 +6,7 @@
test_description='Test commit notes'
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-notes.sh
write_script fake_editor <<\EOF
echo "$MSG" >"$1"
@@ -16,6 +17,11 @@ export GIT_EDITOR
indent=" "
+run_with_limited_time () (
+ { set +x; } 2>/dev/null
+ "$PERL_PATH" -e 'alarm shift; exec @ARGV' -- "$@"
+)
+
test_expect_success 'cannot annotate non-existing HEAD' '
test_must_fail env MSG=3 git notes add
'
@@ -909,6 +915,424 @@ test_expect_success 'displayed notes are used for grep matching' '
test_must_be_empty actual
'
+test_expect_success 'notes.externalCommand shows external notes from protected config' '
+ commit=$(git rev-parse HEAD) &&
+ parent=$(git rev-parse HEAD^) &&
+ rm -f external-notes-starts external-notes-requests &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log -2 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ {
+ printf "%s\n" "$commit" &&
+ printf "%s\n" "$parent"
+ } >expect-requests &&
+ test_cmp expect-requests external-notes-requests &&
+ test_grep "Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ test_grep "^ $parent$" actual
+'
+
+test_expect_success PERL,EXECKEEPSPID 'notes.externalCommand terminates helper during exit cleanup' '
+ commit=$(git rev-parse HEAD) &&
+ test_env TEST_EXTERNAL_NOTES_EXIT_DELAY=10 \
+ run_with_limited_time 2 \
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes -1 >actual &&
+ test_grep "^Notes (external):$" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommandName labels external notes' '
+ commit=$(git rev-parse HEAD) &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ -c notes.externalCommandName=commit-id log -1 >actual &&
+ test_grep "Notes (commit-id):" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommandName is rendered literally' '
+ commit=$(git rev-parse HEAD) &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ -c notes.externalCommandName=refs/notes/commits \
+ log --external-notes -1 >actual &&
+ test_grep "^Notes (refs/notes/commits):$" actual &&
+ ! grep "^Notes:$" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommandTimeoutMs rejects negative values' '
+ test_must_fail git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandTimeoutMs=-1 log -1 2>err &&
+ test_grep "notes.externalCommandTimeoutMs must be non-negative" err
+'
+
+test_expect_success 'notes.externalCommandTimeoutMs times out delayed response' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_DELAY=1 \
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandTimeoutMs=1 \
+ log -1 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
+test_expect_success 'notes.externalCommandTimeoutMs applies to whole response' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_BODY=x \
+ TEST_EXTERNAL_NOTES_CHAR_DELAY=0.02 \
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandTimeoutMs=50 \
+ log -1 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
+test_expect_success PERL,EXECKEEPSPID 'notes.externalCommandTimeoutMs terminates timed-out helper' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_DELAY=10 \
+ run_with_limited_time 2 \
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandTimeoutMs=1 \
+ log -1 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
+test_expect_success 'notes.externalCommandTimeoutMs=0 disables timeout' '
+ commit=$(git rev-parse HEAD) &&
+ test_env TEST_EXTERNAL_NOTES_DELAY=1 \
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandTimeoutMs=0 \
+ log --external-notes -1 >actual &&
+ test_grep "^Notes (external):$" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommand handles CRLF note bodies' '
+ body=$(printf "A\r\nB") &&
+ test_env TEST_EXTERNAL_NOTES_BODY="$body" \
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes -1 >actual &&
+ test_grep "^Notes (external):$" actual &&
+ test_grep "^ B$" actual
+'
+
+test_expect_success 'notes.externalCommand accepts CRLF missing response' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_RESPONSE=missing \
+ TEST_EXTERNAL_NOTES_LINE_ENDING=crlf \
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log -1 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'notes.externalCommand rejects unterminated missing response' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_RESPONSE=missing \
+ TEST_EXTERNAL_NOTES_LINE_ENDING=none \
+ TEST_EXTERNAL_NOTES_EXIT_AFTER_RESPONSE=true \
+ git -c notes.externalCommand="$external_notes_command" \
+ log -1 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
+test_expect_success PERL,EXECKEEPSPID 'notes.externalCommand rejects unterminated live response without deadlock' '
+ git log -1 >expect &&
+ test_env TEST_EXTERNAL_NOTES_RESPONSE=missing \
+ TEST_EXTERNAL_NOTES_LINE_ENDING=none \
+ run_with_limited_time 2 \
+ git -c notes.externalCommand="$external_notes_command" \
+ log -1 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
+test_expect_success 'notes.externalCommand accepts CRLF protocol lines' '
+ commit=$(git rev-parse HEAD) &&
+ test_env TEST_EXTERNAL_NOTES_LINE_ENDING=crlf \
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes -1 >actual &&
+ test_grep "^Notes (external):$" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommand missing response shows no external notes' '
+ write_script external-notes-missing <<-\EOF &&
+ while IFS= read -r commit
+ do
+ printf "%s missing\n" "$commit"
+ done
+ EOF
+ git log -1 >expect &&
+ git -c notes.externalCommand=./external-notes-missing log -1 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'notes.externalCommand empty note shows no external notes' '
+ write_script external-notes-empty <<-\EOF &&
+ while IFS= read -r commit
+ do
+ printf "%s ok 0\n\n" "$commit"
+ done
+ EOF
+ git log -1 >expect &&
+ git -c notes.externalCommand=./external-notes-empty log -1 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'notes.externalCommand rejects invalid note lengths' '
+ write_script external-notes-invalid-length <<-\EOF &&
+ while IFS= read -r commit
+ do
+ printf "%s ok %s\n" "$commit" "$1"
+ done
+ EOF
+ git log -2 >expect &&
+ for bad_length in -1 +1 1x x
+ do
+ git -c notes.externalCommand="./external-notes-invalid-length $bad_length" \
+ log -2 >actual 2>err &&
+ test_cmp expect actual &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err || return 1
+ done
+'
+
+test_expect_success 'notes.externalCommand is suppressed by --no-notes' '
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" log --no-notes -1 >actual &&
+ test_path_is_missing external-notes-starts &&
+ ! grep "Notes (external):" actual
+'
+
+test_expect_success 'notes.externalCommand is suppressed by --no-external-notes' '
+ rm -f external-notes-starts &&
+ git log -1 >expect &&
+ git -c notes.externalCommand="$external_notes_command" \
+ log --no-external-notes -1 >actual &&
+ test_cmp expect actual &&
+ test_path_is_missing external-notes-starts
+'
+
+test_expect_success 'notes.externalCommand combines with explicit notes ref' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --notes=other --external-notes -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes (other):" actual &&
+ test_grep "^ other note$" actual &&
+ test_grep "Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ ! grep "^ order test$" actual
+'
+
+test_expect_success '--show-notes=ref remains additive after --external-notes' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes --show-notes=other -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "^Notes:$" actual &&
+ test_grep "^ order test$" actual &&
+ test_grep "^Notes (other):$" actual &&
+ test_grep "^ other note$" actual &&
+ test_grep "^Notes (external):$" actual &&
+ test_grep "^ $commit$" actual
+'
+
+test_expect_success 'notes.externalCommand can be enabled without default notes refs' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ ! grep "^ order test$" actual &&
+ ! grep "^ other note$" actual
+'
+
+test_expect_success 'notes.externalCommand combines with default notes refs' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --external-notes --notes -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes:" actual &&
+ test_grep "^ order test$" actual &&
+ test_grep "Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ ! grep "^ other note$" actual
+'
+
+test_expect_success 'notes.externalCommand obeys last --external-notes option' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git log --no-notes -1 >expect &&
+ git -c notes.externalCommand="$external_notes_command" \
+ log --external-notes --no-external-notes -1 >actual &&
+ test_cmp expect actual &&
+ test_path_is_missing external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log --notes=other --no-external-notes --external-notes -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes (other):" actual &&
+ test_grep "^ other note$" actual &&
+ test_grep "Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ ! grep "^ order test$" actual
+'
+
+test_expect_success 'notes.externalCommand honors raw notes formatting' '
+ commit=$(git rev-parse HEAD) &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ show -s --format=%N >actual &&
+ test_grep "^$commit$" actual &&
+ ! grep "Notes (external):" actual
+'
+
+test_expect_success 'format-patch --external-notes includes external notes only' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ format-patch --external-notes -1 --stdout >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "^Notes (external):" actual &&
+ test_grep "^ $commit$" actual &&
+ ! grep "^ order test$" actual
+'
+
+test_expect_success 'notes.externalCommand is not used for grep matching' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ log --grep="$commit" >actual &&
+ test_must_be_empty actual &&
+ test_path_is_missing external-notes-starts
+'
+
+test_expect_success 'notes.externalCommandForGrep includes external notes in grep matching' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ -c notes.externalCommandForGrep=true \
+ log --grep="$commit" -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes (external):" actual
+'
+
+test_expect_success 'notes.externalCommandForGrep does not search hidden notes' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandForGrep=true \
+ log --oneline --grep="$commit" -1 >actual &&
+ test_must_be_empty actual &&
+ test_path_is_missing external-notes-starts
+'
+
+test_expect_success 'notes.externalCommandForGrep honors --no-external-notes' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ -c notes.externalCommandForGrep=true \
+ log --no-external-notes --grep="$commit" -1 >actual &&
+ test_must_be_empty actual &&
+ test_path_is_missing external-notes-starts
+'
+
+test_expect_success 'notes.externalCommandForGrep combines with explicit notes ref' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ -c notes.externalCommandForGrep=true \
+ log --notes=other --external-notes --grep="$commit" -1 >actual &&
+ test_line_count = 1 external-notes-starts &&
+ test_grep "Notes (external):" actual &&
+ test_grep "Notes (other):" actual &&
+ ! grep "^ order test$" actual
+'
+
+test_expect_success 'notes.externalCommandForGrep is ignored from local config' '
+ commit=$(git rev-parse HEAD) &&
+ rm -f external-notes-starts &&
+ test_config notes.externalCommandForGrep true &&
+ git -c notes.externalCommand="$external_notes_command" \
+ log --grep="$commit" >actual &&
+ test_must_be_empty actual &&
+ test_path_is_missing external-notes-starts
+'
+
+test_expect_success 'notes.externalCommand is not used with explicit notes ref' '
+ rm -f external-notes-starts &&
+ git -c notes.externalCommand="$external_notes_command" log --notes=other -1 >actual &&
+ test_path_is_missing external-notes-starts &&
+ ! grep "Notes (external):" actual
+'
+
+test_expect_success 'notes.externalCommand is ignored from local config' '
+ rm -f external-notes-starts &&
+ test_config notes.externalCommand "$external_notes_command" &&
+ git log -1 >actual &&
+ test_path_is_missing external-notes-starts &&
+ ! grep "Notes (external):" actual
+'
+
+test_expect_success 'notes.externalCommandName is ignored from local config' '
+ test_config notes.externalCommandName local &&
+ git -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ log -1 >actual &&
+ test_grep "Notes (external):" actual &&
+ ! grep "Notes (local):" actual
+'
+
+test_expect_success 'external_notes_reset clears cached helper config' '
+ test-tool notes-external-config-reset >actual &&
+ cat >expect <<-\EOF &&
+ configured=0
+ name=external
+ timeout_ms=100
+ grep=0
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'notes.externalCommand warning is shown once' '
+ write_script external-notes-fail <<-\EOF &&
+ while IFS= read -r commit
+ do
+ printf "%s-mismatch missing\n" "$commit"
+ done
+ EOF
+ git -c notes.externalCommand=./external-notes-fail log -2 >actual 2>err &&
+ test_grep "notes.externalCommand failed" err &&
+ test_line_count = 1 err
+'
+
test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' '
test_config core.notesRef refs/notes/other &&
echo "Note on a tree" >expect &&
diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh
index 8ee3d2c37d02..abbdb42dc9f7 100755
--- a/t/t6120-describe.sh
+++ b/t/t6120-describe.sh
@@ -15,6 +15,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-notes.sh
check_describe () {
indir= &&
@@ -867,6 +868,22 @@ test_expect_success 'format-rev with %N (note)' '
test_cmp expect actual
'
+test_expect_success 'format-rev with %N uses external notes' '
+ commit=$(git -C repo-format rev-parse HEAD) &&
+ rm -f repo-format/format-rev-external-notes-starts \
+ repo-format/format-rev-external-notes-requests &&
+ printf "%s\n" "$commit" >input &&
+ printf "%s\n\n" "$commit" >expect &&
+ TEST_EXTERNAL_NOTES_PREFIX=format-rev-external-notes \
+ git -C repo-format -c notes.externalCommand="$external_notes_command" \
+ $external_notes_command_timeout_config \
+ format-rev --stdin-mode=text --format="tformat:%N" \
+ <input >actual &&
+ test_line_count = 1 repo-format/format-rev-external-notes-starts &&
+ test_cmp input repo-format/format-rev-external-notes-requests &&
+ test_cmp expect actual
+'
+
test_expect_success 'format-rev --notes<ref> (custom notes ref)' '
# One custom notes ref
test_when_finished "git -C repo-format notes remove" &&
--
2.53.0
^ permalink raw reply related
* [PATCH v3 3/4] t3301: cover generic displayed notes behavior
From: Siddh Raman Pant @ 2026-05-23 10:38 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Elijah Newren, brian m. carlson, Jeff King, Johannes Sixt,
Oswald Buddenhagen
In-Reply-To: <cover.1779532562.git.siddh.raman.pant@oracle.com>
Displayed notes already participate in common log behavior.
Add explicit coverage for raw notes formatting, --no-notes
suppression, explicit notes refs, and --grep matching before
teaching external notes to feed the same display path.
Assisted-by: Codex:gpt-5.5-xhigh-fast
Signed-off-by: Siddh Raman Pant <siddh.raman.pant@oracle.com>
---
t/t3301-notes.sh | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/t/t3301-notes.sh b/t/t3301-notes.sh
index d6c50460d086..27439010dfbc 100755
--- a/t/t3301-notes.sh
+++ b/t/t3301-notes.sh
@@ -885,6 +885,30 @@ test_expect_success '--show-notes=ref accumulates' '
test_cmp expect-both-reversed actual
'
+test_expect_success 'displayed notes honor raw notes formatting' '
+ git show -s --format=%N >actual &&
+ test_grep "^order test$" actual &&
+ ! grep "Notes" actual
+'
+
+test_expect_success 'displayed notes are suppressed by --no-notes' '
+ git log --no-notes -1 >actual &&
+ test_cmp expect-not-other actual
+'
+
+test_expect_success 'explicit notes ref replaces default displayed notes' '
+ git log --notes=other -1 >actual &&
+ test_cmp expect-other actual
+'
+
+test_expect_success 'displayed notes are used for grep matching' '
+ commit=$(git rev-parse HEAD) &&
+ git log --grep="order test" -1 >actual &&
+ test_grep "^commit $commit$" actual &&
+ git log --no-notes --grep="order test" -1 >actual &&
+ test_must_be_empty actual
+'
+
test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' '
test_config core.notesRef refs/notes/other &&
echo "Note on a tree" >expect &&
--
2.53.0
^ permalink raw reply related
* [PATCH v3 1/4] notes: convert raw arg in format_display_notes() to bool
From: Siddh Raman Pant @ 2026-05-23 10:38 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Elijah Newren, brian m. carlson, Jeff King, Johannes Sixt,
Oswald Buddenhagen
In-Reply-To: <cover.1779532562.git.siddh.raman.pant@oracle.com>
It's used as a boolean flag, let's not use an int.
Signed-off-by: Siddh Raman Pant <siddh.raman.pant@oracle.com>
---
log-tree.c | 3 +--
notes.c | 6 +++---
notes.h | 2 +-
revision.c | 2 +-
4 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/log-tree.c b/log-tree.c
index 7e048701d0c5..4503a42dde6b 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -854,10 +854,9 @@ void show_log(struct rev_info *opt)
}
if (opt->show_notes) {
- int raw;
struct strbuf notebuf = STRBUF_INIT;
+ bool raw = (opt->commit_format == CMIT_FMT_USERFORMAT);
- raw = (opt->commit_format == CMIT_FMT_USERFORMAT);
format_display_notes(&commit->object.oid, ¬ebuf,
get_log_output_encoding(), raw);
ctx.notes_message = strbuf_detach(¬ebuf, NULL);
diff --git a/notes.c b/notes.c
index 8f315e2a00d2..201f1df3dc29 100644
--- a/notes.c
+++ b/notes.c
@@ -1273,11 +1273,11 @@ void free_notes(struct notes_tree *t)
* If the given notes_tree is NULL, the internal/default notes_tree will be
* used instead.
*
- * (raw != 0) gives the %N userformat; otherwise, the note message is given
+ * (raw == true) gives the %N userformat; otherwise, the note message is given
* for human consumption.
*/
static void format_note(struct notes_tree *t, const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, int raw)
+ struct strbuf *sb, const char *output_encoding, bool raw)
{
static const char utf8[] = "utf-8";
const struct object_id *oid;
@@ -1338,7 +1338,7 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid
}
void format_display_notes(const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, int raw)
+ struct strbuf *sb, const char *output_encoding, bool raw)
{
int i;
assert(display_notes_trees);
diff --git a/notes.h b/notes.h
index 6dc6d7b26548..f6410b31e1c9 100644
--- a/notes.h
+++ b/notes.h
@@ -313,7 +313,7 @@ void load_display_notes(struct display_notes_opt *opt);
* You *must* call load_display_notes() before using this function.
*/
void format_display_notes(const struct object_id *object_oid,
- struct strbuf *sb, const char *output_encoding, int raw);
+ struct strbuf *sb, const char *output_encoding, bool raw);
/*
* Load the notes tree from each ref listed in 'refs'. The output is
diff --git a/revision.c b/revision.c
index 599b3a66c369..cd9fcefa0a88 100644
--- a/revision.c
+++ b/revision.c
@@ -4107,7 +4107,7 @@ static int commit_match(struct commit *commit, struct rev_info *opt)
if (opt->show_notes) {
if (!buf.len)
strbuf_addstr(&buf, message);
- format_display_notes(&commit->object.oid, &buf, encoding, 1);
+ format_display_notes(&commit->object.oid, &buf, encoding, true);
}
/*
--
2.53.0
^ permalink raw reply related
* [PATCH v3 2/4] wrapper: add support for timeout and deadline in read helpers
From: Siddh Raman Pant @ 2026-05-23 10:38 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Elijah Newren, brian m. carlson, Jeff King, Johannes Sixt,
Oswald Buddenhagen
In-Reply-To: <cover.1779532562.git.siddh.raman.pant@oracle.com>
Add read helpers which allow a caller to enforce a timeout per read,
and a deadline for the read in case multiple reads have to be done
under a common timeout.
Assisted-by: Codex:gpt-5.5-xhigh-fast
Signed-off-by: Siddh Raman Pant <siddh.raman.pant@oracle.com>
---
strbuf.c | 26 +++++++++-
strbuf.h | 4 ++
wrapper.c | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++----
wrapper.h | 23 +++++++++
4 files changed, 182 insertions(+), 10 deletions(-)
diff --git a/strbuf.c b/strbuf.c
index 3e04addc22fe..b3fc7c624aa2 100644
--- a/strbuf.c
+++ b/strbuf.c
@@ -749,13 +749,15 @@ int strbuf_getline_nul(struct strbuf *sb, FILE *fp)
return strbuf_getdelim(sb, fp, '\0');
}
-int strbuf_getwholeline_fd(struct strbuf *sb, int fd, int term)
+static int strbuf_getwholeline_fd_with(struct strbuf *sb, int fd, int term,
+ xread_cb_t xread_cb,
+ void *cb_data)
{
strbuf_reset(sb);
while (1) {
char ch;
- ssize_t len = xread(fd, &ch, 1);
+ ssize_t len = xread_cb(fd, &ch, 1, cb_data);
if (len <= 0)
return EOF;
strbuf_addch(sb, ch);
@@ -765,6 +767,26 @@ int strbuf_getwholeline_fd(struct strbuf *sb, int fd, int term)
return 0;
}
+int strbuf_getwholeline_fd_deadline(struct strbuf *sb, int fd, int term,
+ uint64_t deadline_ns)
+{
+ return strbuf_getwholeline_fd_with(sb, fd, term, xread_deadline_fn,
+ &deadline_ns);
+}
+
+int strbuf_getwholeline_fd_timeout(struct strbuf *sb, int fd, int term,
+ int timeout_ms)
+{
+ return strbuf_getwholeline_fd_with(sb, fd, term, xread_timeout_fn,
+ &timeout_ms);
+}
+
+/* Non-timeout version for compatibility. */
+int strbuf_getwholeline_fd(struct strbuf *sb, int fd, int term)
+{
+ return strbuf_getwholeline_fd_timeout(sb, fd, term, 0);
+}
+
ssize_t strbuf_read_file(struct strbuf *sb, const char *path, size_t hint)
{
int fd;
diff --git a/strbuf.h b/strbuf.h
index 06e284f9cca4..f896da1277a6 100644
--- a/strbuf.h
+++ b/strbuf.h
@@ -535,6 +535,10 @@ int strbuf_appendwholeline(struct strbuf *sb, FILE *file, int term);
* descriptor.
*/
int strbuf_getwholeline_fd(struct strbuf *sb, int fd, int term);
+int strbuf_getwholeline_fd_timeout(struct strbuf *sb, int fd, int term,
+ int timeout_ms);
+int strbuf_getwholeline_fd_deadline(struct strbuf *sb, int fd, int term,
+ uint64_t deadline_ns);
/**
* Set the buffer to the path of the current working directory.
diff --git a/wrapper.c b/wrapper.c
index 16f5a63fbb61..1f42845e031e 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -9,6 +9,7 @@
#include "parse.h"
#include "gettext.h"
#include "strbuf.h"
+#include "trace.h"
#include "trace2.h"
#ifdef HAVE_RTLGENRANDOM
@@ -220,28 +221,129 @@ static int handle_nonblock(int fd, short poll_events, int err)
return 1;
}
-/*
- * xread() is the same a read(), but it automatically restarts read()
- * operations with a recoverable error (EAGAIN and EINTR). xread()
+static int wait_for_fd(int fd, short poll_events, int timeout_ms)
+{
+ struct pollfd pfd;
+
+ if (timeout_ms < 0) {
+ /* Negative timeout makes no sense. */
+ errno = EINVAL;
+ return -1;
+ }
+
+ pfd.fd = fd;
+ pfd.events = poll_events;
+
+ while(1) {
+ int ret = poll(&pfd, 1, timeout_ms);
+
+ if (ret <= 0) {
+ /* Retry if interrupted. */
+ if (ret < 0 && errno == EINTR)
+ continue;
+
+ /* Set errno if timeout happened. */
+ if (ret == 0)
+ errno = ETIMEDOUT;
+
+ return -1;
+ }
+
+ /* Invalid FD passed. */
+ if (pfd.revents & POLLNVAL) {
+ errno = EBADF;
+ return -1;
+ }
+
+ /* Some error happened. */
+ if (pfd.revents & POLLERR) {
+ errno = EIO;
+ return -1;
+ }
+
+ /* HangUp => We are ready to consume output till EOF. */
+ if (pfd.revents & (poll_events | POLLHUP))
+ return 0;
+ }
+}
+
+/**
+ * xread_timeout() is the same as read(), but it automatically restarts read()
+ * operations with a recoverable error (EAGAIN and EINTR). xread_timeout()
* DOES NOT GUARANTEE that "len" bytes is read even if the data is available.
+ *
+ * Fails with ETIMEDOUT when no bytes become available within timeout_ms
+ * milliseconds. A zero timeout disables timeout handling, so reads can
+ * block until the file descriptor is readable. Negative timeouts are invalid.
*/
-ssize_t xread(int fd, void *buf, size_t len)
+ssize_t xread_timeout(int fd, void *buf, size_t len, int timeout_ms)
{
ssize_t nr;
+
if (len > MAX_IO_SIZE)
len = MAX_IO_SIZE;
+
while (1) {
+ if (timeout_ms && wait_for_fd(fd, POLLIN, timeout_ms))
+ return -1;
+
nr = read(fd, buf, len);
+
if (nr < 0) {
if (errno == EINTR)
continue;
- if (handle_nonblock(fd, POLLIN, errno))
- continue;
+
+ if (timeout_ms) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
+ continue;
+ } else {
+ if (handle_nonblock(fd, POLLIN, errno))
+ continue;
+ }
}
+
return nr;
}
}
+/* Non-timeout version for compatibility. */
+ssize_t xread(int fd, void *buf, size_t len)
+{
+ return xread_timeout(fd, buf, len, 0);
+}
+
+static int remaining_timeout_ms(uint64_t deadline_ns)
+{
+ uint64_t now, remaining_ns;
+
+ if (!deadline_ns)
+ return 0;
+
+ now = getnanotime();
+ if (now >= deadline_ns) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+
+ remaining_ns = deadline_ns - now;
+ return (int)((remaining_ns + 999999ULL) / 1000000ULL);
+}
+
+/* (deadline_ns = 0) disables the deadline and short-circuits to xread(). */
+ssize_t xread_deadline(int fd, void *buf, size_t len, uint64_t deadline_ns)
+{
+ int timeout_ms;
+
+ if (deadline_ns == 0)
+ return xread(fd, buf, len);
+
+ timeout_ms = remaining_timeout_ms(deadline_ns);
+ if (timeout_ms < 0)
+ return -1;
+
+ return xread_timeout(fd, buf, len, timeout_ms);
+}
+
/*
* xwrite() is the same a write(), but it automatically restarts write()
* operations with a recoverable error (EAGAIN and EINTR). xwrite() DOES NOT
@@ -283,13 +385,15 @@ ssize_t xpread(int fd, void *buf, size_t len, off_t offset)
}
}
-ssize_t read_in_full(int fd, void *buf, size_t count)
+static ssize_t read_in_full_with(int fd, void *buf, size_t count,
+ xread_cb_t xread_cb,
+ void *cb_data)
{
char *p = buf;
ssize_t total = 0;
while (count > 0) {
- ssize_t loaded = xread(fd, p, count);
+ ssize_t loaded = xread_cb(fd, p, count, cb_data);
if (loaded < 0)
return -1;
if (loaded == 0)
@@ -302,6 +406,25 @@ ssize_t read_in_full(int fd, void *buf, size_t count)
return total;
}
+ssize_t read_in_full_deadline(int fd, void *buf, size_t count,
+ uint64_t deadline_ns)
+{
+ return read_in_full_with(fd, buf, count, xread_deadline_fn,
+ &deadline_ns);
+}
+
+ssize_t read_in_full_timeout(int fd, void *buf, size_t count, int timeout_ms)
+{
+ return read_in_full_with(fd, buf, count, xread_timeout_fn,
+ &timeout_ms);
+}
+
+/* Non-timeout version for compatibility. */
+ssize_t read_in_full(int fd, void *buf, size_t count)
+{
+ return read_in_full_timeout(fd, buf, count, 0);
+}
+
ssize_t write_in_full(int fd, const void *buf, size_t count)
{
const char *p = buf;
diff --git a/wrapper.h b/wrapper.h
index 15ac3bab6e97..10d85c467b86 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -15,6 +15,8 @@ const char *mmap_os_err(void);
void *xmmap_gently(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int xopen(const char *path, int flags, ...);
ssize_t xread(int fd, void *buf, size_t len);
+ssize_t xread_timeout(int fd, void *buf, size_t len, int timeout_ms);
+ssize_t xread_deadline(int fd, void *buf, size_t len, uint64_t deadline_ns);
ssize_t xwrite(int fd, const void *buf, size_t len);
ssize_t xpread(int fd, void *buf, size_t len, off_t offset);
int xdup(int fd);
@@ -44,9 +46,30 @@ int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
int git_mkstemp_mode(char *pattern, int mode);
ssize_t read_in_full(int fd, void *buf, size_t count);
+ssize_t read_in_full_timeout(int fd, void *buf, size_t count, int timeout_ms);
+ssize_t read_in_full_deadline(int fd, void *buf, size_t count,
+ uint64_t deadline_ns);
ssize_t write_in_full(int fd, const void *buf, size_t count);
ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);
+typedef ssize_t xread_cb_t(int fd, void *buf, size_t len, const void *cb_data);
+
+static inline ssize_t xread_timeout_fn(int fd, void *buf, size_t len,
+ const void *cb_data)
+{
+ const int *timeout_ms = cb_data;
+
+ return xread_timeout(fd, buf, len, *timeout_ms);
+}
+
+static inline ssize_t xread_deadline_fn(int fd, void *buf, size_t len,
+ const void *cb_data)
+{
+ const uint64_t *deadline_ns = cb_data;
+
+ return xread_deadline(fd, buf, len, *deadline_ns);
+}
+
static inline ssize_t write_str_in_full(int fd, const char *str)
{
return write_in_full(fd, str, strlen(str));
--
2.53.0
^ permalink raw reply related
* [PATCH v3 0/4] Add support for an external command for fetching notes
From: Siddh Raman Pant @ 2026-05-23 10:38 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Patrick Steinhardt,
Elijah Newren, brian m. carlson, Jeff King, Johannes Sixt,
Oswald Buddenhagen
v2: https://lore.kernel.org/git/cover.1779464886.git.siddh.raman.pant@oracle.com/
v1: https://lore.kernel.org/git/cover.1779207350.git.siddh.raman.pant@oracle.com/
<...insert text from v1 cover here...>
Changes since v2:
- Removed stale help text talking about force-killing helper process.
Changes since v1:
- Removed Documentation commit and sent as a standalone patch.
- Removed finish_command_with_timeout addition (and thus sleep_nanosec).
- Squashed the external notes command code, doc, and test commits.
- Removed horizontal separators from note-external.c.
- Removed global variables from translation unit and instead store config in
a dedicated new struct member in struct display_notes_opt.
- Reworded the main commit to have better explanation of the motivation.
Siddh Raman Pant (4):
notes: convert raw arg in format_display_notes() to bool
wrapper: add support for timeout and deadline in read helpers
t3301: cover generic displayed notes behavior
notes: support an external command to display notes
Documentation/config/notes.adoc | 59 +++
Documentation/git-format-patch.adoc | 11 +-
Documentation/git-range-diff.adoc | 6 +
Documentation/pretty-options.adoc | 9 +
Makefile | 2 +
builtin/log.c | 17 +-
builtin/name-rev.c | 9 +-
builtin/range-diff.c | 2 +
contrib/completion/git-completion.bash | 4 +-
log-tree.c | 10 +-
meson.build | 1 +
notes-external.c | 414 ++++++++++++++++++
notes-external.h | 53 +++
notes.c | 266 +++++++++---
notes.h | 33 +-
revision.c | 36 +-
strbuf.c | 26 +-
strbuf.h | 4 +
t/helper/meson.build | 1 +
t/helper/test-external-notes | 64 +++
t/helper/test-notes-external-config-reset.c | 24 ++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/lib-notes.sh | 19 +
t/t3206-range-diff.sh | 68 +++
t/t3301-notes.sh | 448 ++++++++++++++++++++
t/t6120-describe.sh | 17 +
wrapper.c | 139 +++++-
wrapper.h | 23 +
29 files changed, 1691 insertions(+), 76 deletions(-)
create mode 100644 notes-external.c
create mode 100644 notes-external.h
create mode 100755 t/helper/test-external-notes
create mode 100644 t/helper/test-notes-external-config-reset.c
create mode 100644 t/lib-notes.sh
--
2.53.0
^ permalink raw reply
* Re: [PATCH v2 4/4] notes: support an external command to display notes
From: Siddh Raman Pant @ 2026-05-23 10:32 UTC (permalink / raw)
To: git@vger.kernel.org
Cc: oswald.buddenhagen@gmx.de, gitster@pobox.com,
code@khaugsbakk.name, j6t@kdbg.org, peff@peff.net,
newren@gmail.com, ps@pks.im, sandals@crustytoothpaste.net
In-Reply-To: <1fb0666e0d950a06f605a5af4fe5555c9b1008d2.1779464886.git.siddh.raman.pant@oracle.com>
[-- Attachment #1: Type: text/plain, Size: 168 bytes --]
I had updated the help text and then later dropped the force-kill
commits, but forgot to update the help text.
I'll send a v3, sorry about that.
Thanks,
Siddh
[-- Attachment #2: This is a digitally signed message part --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
^ permalink raw reply
* What's cooking in git.git (May 2026, #06)
From: Junio C Hamano @ 2026-05-23 9:06 UTC (permalink / raw)
To: git
Here are the topics that have been cooking in my tree. Commits
prefixed with '+' are in 'next' (being in 'next' is a sign that a
topic is stable enough to be used and is a candidate to be in a
future release). Commits prefixed with '-' are only in 'seen', and
aren't considered "accepted" at all and may be annotated with a URL
to a message that raises issues but they are by no means exhaustive.
A topic without enough support may be discarded after a long period
of no activity (of course they can be resubmitted when new interests
arise).
Copies of the source code to Git live in many repositories, and the
following is a list of the ones I push into or their mirrors. Some
repositories have only a subset of branches.
With maint, master, next, seen, todo:
git://git.kernel.org/pub/scm/git/git.git/
git://repo.or.cz/alt-git.git/
https://kernel.googlesource.com/pub/scm/git/git/
https://github.com/git/git/
https://gitlab.com/git-scm/git/
With all the integration branches and topics broken out:
https://github.com/gitster/git/
Even though the preformatted documentation in HTML and man format
are not sources, they are published in these repositories for
convenience (replace "htmldocs" with "manpages" for the manual
pages):
git://git.kernel.org/pub/scm/git/git-htmldocs.git/
https://github.com/gitster/git-htmldocs.git/
Release tarballs are available at:
https://www.kernel.org/pub/software/scm/git/
--------------------------------------------------
[Graduated to 'master']
* aw/validate-proxy-url-scheme (2026-05-05) 1 commit
(merged to 'next' on 2026-05-15 at da9c1b71d7)
+ http: reject unsupported proxy URL schemes
Misspelt proxy URL (e.g., httt://...) did not trigger any warning
or failure, which has been corrected.
source: <20260505091941.1825-2-aminnimaj@gmail.com>
* jc/ci-enable-expensive (2026-05-10) 2 commits
(merged to 'next' on 2026-05-15 at d258bb5e55)
+ ci: enable EXPENSIVE for contributor builds
+ Merge branch 'js/objects-larger-than-4gb-on-windows' into jc/ci-enable-expensive
Enable expensive tests to catch topics that may cause breakages on
integration branches closer to their origin in the contributor PR
builds.
source: <xmqqjyta9630.fsf@gitster.g>
* jk/apply-leakfix (2026-05-15) 1 commit
(merged to 'next' on 2026-05-20 at 725a20bf93)
+ apply: plug leak on "patch too large" error
Leakfix.
source: <20260516021622.GA744303@coredump.intra.peff.net>
* jk/commit-sign-overflow-fix (2026-05-15) 1 commit
(merged to 'next' on 2026-05-20 at e1a320d4e5)
+ commit: handle large commit messages in utf8 verification
Leakfix.
source: <20260516022310.GB744303@coredump.intra.peff.net>
* kh/doc-log-decorate-list (2026-04-27) 2 commits
(merged to 'next' on 2026-05-15 at f740311a37)
+ doc: log: use the same delimiter in description list
+ doc: log: fix --decorate description list
Doc update.
cf. <xmqqpl31np0l.fsf@gitster.g>
source: <CV_doc_log_--decorate_list.626@msgid.xyz>
* kn/refs-generic-helpers (2026-05-04) 9 commits
(merged to 'next' on 2026-05-15 at 62cb4e0ce2)
+ refs: use peeled tag values in reference backends
+ refs: add peeled object ID to the `ref_update` struct
+ refs: move object parsing to the generic layer
+ update-ref: handle rejections while adding updates
+ update-ref: move `print_rejected_refs()` up
+ refs: return `ref_transaction_error` from `ref_transaction_update()`
+ refs: extract out reflog config to generic layer
+ refs: introduce `ref_store_init_options`
+ refs: remove unused typedef 'ref_transaction_commit_fn'
Refactor service routines in the ref subsystem backends.
cf. <afmFmGo_Sg33Rv6V@pks.im>
cf. <87o6isqq4q.fsf@toon--20250203-5JQV3.mail-host-address-is-not-set>
source: <20260504-refs-move-to-generic-layer-v4-0-936ac2f0b1a3@gmail.com>
* mm/git-url-parse (2026-05-01) 8 commits
(merged to 'next' on 2026-05-15 at 416deceeeb)
+ t9904: add tests for the new url-parse builtin
+ doc: describe the url-parse builtin
+ builtin: create url-parse command
+ urlmatch: define url_parse function
+ url: return URL_SCHEME_UNKNOWN instead of dying
+ url: move scheme detection to URL header/source
+ url: move url_is_local_not_ssh to url.h
+ connect: rename enum protocol to url_scheme
The internal URL parsing logic has been made accessible via a new
subcommand "git url-parse".
cf. <xmqqjyt9p9pk.fsf@gitster.g>
cf. <20260512085734.GA26769@tb-raspi4>
source: <pull.1715.v3.git.git.1777699722.gitgitgadget@gmail.com>
* ps/maintenance-daemonize-lockfix (2026-05-13) 2 commits
(merged to 'next' on 2026-05-21 at 9b7fa37559)
+ run-command: honor "gc.auto" for auto-maintenance
+ builtin/maintenance: fix locking with "--detach"
"git maintenance" that goes background did not use the lockfile to
prevent multiple maintenance processes from running at the same
time, which has been corrected.
cf. <ag1MHje6-C6nmcO4@pks.im>
source: <20260513-pks-maintenance-fix-lock-with-detach-v3-0-f27a1ac82891@pks.im>
* pw/xdiff-shrink-memory-consumption (2026-05-04) 5 commits
(merged to 'next' on 2026-05-15 at 7a867909d2)
+ xdiff: reduce the size of array
+ xprepare: simplify error handling
+ xdiff: cleanup xdl_clean_mmatch()
+ xdiff: reduce size of action arrays
+ Merge branch 'en/xdiff-cleanup-3' into pw/xdiff-shrink-memory-consumption
Shrink wasted memory in Myers diff that does not account for common
prefix and suffix removal.
source: <cover.1777903579.git.phillip.wood@dunelm.org.uk>
* sp/shallow-deepen-on-non-shallow-repo-fix (2026-05-11) 1 commit
(merged to 'next' on 2026-05-15 at 67dd491aae)
+ shallow: fix relative deepen on non-shallow repositories
"git fetch --deepen=<n>" in a full clone truncated the history to <n>
commits deep, which has been corrected to be a no-op instead.
source: <20260511192044.169557-1-samo_pogacnik@t-2.net>
* za/t2000-modernise-more (2026-04-29) 1 commit
(merged to 'next' on 2026-05-15 at 3b524d0ba5)
+ t2000: consolidate second scenario into a single test block
Test update.
cf. <xmqqfr3xnofn.fsf@gitster.g>
source: <20260429103607.406339-1-zakariyahali100@gmail.com>
--------------------------------------------------
[New Topics]
* gh/jump-auto-mode (2026-05-21) 1 commit
- git-jump: pick a mode automatically when invoked without arguments
The 'git-jump' command (in contrib/) has been taught to automatically
pick a mode (merge, diff, or ws) when invoked without arguments.
Comments?
source: <pull.2108.v3.git.1779371110195.gitgitgadget@gmail.com>
* sp/doc-range-diff-takes-notes (2026-05-20) 1 commit
(merged to 'next' on 2026-05-22 at 020bec81b7)
+ Documentation/git-range-diff: add missing notes options in synopsis
Doxfix.
Will merge to 'master'.
source: <20260521052841.73775-1-siddh.raman.pant@oracle.com>
* ps/odb-source-loose (2026-05-21) 19 commits
- odb/source-loose: drop pointer to the "files" source
- odb/source-loose: stub out remaining callbacks
- odb/source-loose: wire up `write_object_stream()` callback
- object-file: refactor writing objects to use loose source
- odb/source-loose: wire up `write_object()` callback
- loose: refactor object map to operate on `struct odb_source_loose`
- odb/source-loose: wire up `freshen_object()` callback
- odb/source-loose: drop `odb_source_loose_has_object()`
- odb/source-loose: wire up `count_objects()` callback
- odb/source-loose: wire up `find_abbrev_len()` callback
- odb/source-loose: wire up `for_each_object()` callback
- odb/source-loose: wire up `read_object_stream()` callback
- odb/source-loose: wire up `read_object_info()` callback
- odb/source-loose: wire up `close()` callback
- odb/source-loose: wire up `reprepare()` callback
- odb/source-loose: start converting to a proper `struct odb_source`
- odb/source-loose: store pointer to "files" instead of generic source
- odb/source-loose: move loose source into "odb/" subsystem
- Merge branch 'ps/odb-in-memory' into ps/odb-source-loose
(this branch uses jt/odb-transaction-write and ps/odb-in-memory.)
The loose object source has been refactored into a proper `struct
odb_source`.
Comments?
source: <20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im>
* ps/setup-centralize-odb-creation (2026-05-21) 9 commits
- setup: construct object database in `apply_repository_format()`
- repository: stop reading loose object map twice on repo init
- setup: stop initializing object database without repository
- setup: stop creating the object database in `setup_git_env()`
- repository: stop initializing the object database in `repo_set_gitdir()`
- setup: deduplicate logic to apply repository format
- setup: drop `setup_git_env()`
- t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY
- Merge branch 'ps/setup-wo-the-repository' into ps/setup-centralize-odb-creation
(this branch uses ps/setup-wo-the-repository.)
The setup logic to discover and configure repositories has been
refactored, and the initialization of the object database has been
centralized.
Comments?
source: <20260521-b4-pks-setup-centralize-odb-creation-v1-0-f130d2a7e8ae@pks.im>
* ps/gitlab-ci-macOS-improvements (2026-05-21) 2 commits
(merged to 'next' on 2026-05-22 at aaa3c7021e)
+ gitlab-ci: update macOS image
+ gitlab-ci: upgrade macOS runners
Update GitLab CI jobs that exercise macOS.
Will merge to 'master'.
source: <20260521-b4-pks-gitlab-ci-updates-v1-0-53bb46ed33e0@pks.im>
* kh/doc-hook (2026-05-21) 4 commits
- doc: hook: don’t self-link via config include
- doc: config: include existing git-hook(1) section
- doc: hook: consistently capitalize Git
- doc: hook: remove stray backtick
Doc updates.
Comments?
source: <CV_doc_hook.6f0@msgid.xyz>
* kh/doc-replay-config (2026-05-21) 4 commits
- doc: replay: move “default” to the right-hand-side
- doc: replay: use a nested definition list
- doc: replay: simplify replay.refAction description
- doc: link to config for git-replay(1)
Doc update for "git replay" to actually refer to its configuration
variables.
Comments?
source: <CV_doc_replay_config.709@msgid.xyz>
--------------------------------------------------
[Cooking]
* jk/commit-graph-lazy-load-fallback (2026-05-18) 1 commit
(merged to 'next' on 2026-05-22 at d1188df466)
+ commit: fall back to full read when maybe_tree is NULL
The logic to lazy-load trees from the commit-graph has been made
more robust by falling back to reading the commit object when
the commit-graph is no longer available.
Will merge to 'master'.
source: <20260519061534.GA1709881@coredump.intra.peff.net>
* jk/connect-service-enum (2026-05-21) 2 commits
- transport-helper: fix typo in BUG() message
(merged to 'next' on 2026-05-21 at fd80c61e21)
+ connect: use "service" enum for "name" argument
The "name" argument in git_connect() and related functions has been
converted to a "service" enum to improve type safety and clarify its
purpose.
Will merge to 'next' and then to 'master'.
source: <20260519052219.GA1703179@coredump.intra.peff.net>
source: <20260522044352.GA861761@coredump.intra.peff.net>
* jk/sq-dequote-cleanup (2026-05-18) 3 commits
(merged to 'next' on 2026-05-21 at fbedf2daea)
+ quote: simplify internals of dequoting
+ quote: drop sq_dequote_to_argv()
+ quote.h: bump strvec forward declaration to the top
Code simplification.
Will merge to 'master'.
source: <20260519011837.GA1615637@coredump.intra.peff.net>
* aj/stash-patch-optimize-temporary-index (2026-05-19) 1 commit
- stash: reuse cached index entries in --patch temporary index
"git stash -p" has been optimized by reusing cached index
entries in its temporary index, avoiding unnecessary lstat()
calls on unchanged files.
source: <pull.2306.git.git.1779194605735.gitgitgadget@gmail.com>
* tb/bitmap-build-performance (2026-05-19) 9 commits
- pack-bitmap: build pseudo-merge bitmaps after regular bitmaps
- pack-bitmap: remember pseudo-merge parents
- pack-bitmap: sort bitmaps before XORing
- pack-bitmap: cache object positions during fill
- pack-bitmap: consolidate `find_object_pos()` success path
- pack-bitmap: reuse stored selected bitmaps
- pack-bitmap: check subtree bits before recursing
- pack-bitmap: pass object position to `fill_bitmap_tree()`
- Merge branch 'tb/pseudo-merge-bugfixes' into tb/bitmap-build-performance
(this branch uses tb/pseudo-merge-bugfixes.)
Reachability bitmap generation has been significantly optimized. By
reordering tree traversal, caching object positions, and refining how
pseudo-merge bitmaps are constructed, the performance of "git repack
--write-midx-bitmaps" is improved, especially for large repositories
and when using pseudo-merges.
source: <cover.1779207127.git.me@ttaylorr.com>
* hn/status-pull-advice-qualified (2026-05-21) 1 commit
- remote: qualify "git pull" advice for non-upstream compareBranches
Advice shown by "git status" when the local branch is behind or has
diverged from its push branch has been updated to suggest "git pull
<remote> <branch>".
Comments?
source: <pull.2301.v4.git.git.1779372367317.gitgitgadget@gmail.com>
* jk/dumb-http-alternate-fix (2026-05-12) 1 commit
(merged to 'next' on 2026-05-17 at c1a51214fb)
+ http: handle absolute-path alternates from server root
The HTTP walker misinterpreted the alternates file that gives an
absolute path when the server URL does not have the final slash
(i.e., "https://example.com" not "https://example.com/").
Will merge to 'master'.
source: <20260512162619.GA69813@coredump.intra.peff.net>
* jk/pretty-no-strbuf-presizing (2026-05-12) 1 commit
(merged to 'next' on 2026-05-17 at ee684c614f)
+ pretty: drop strbuf pre-sizing from add_rfc2047()
Remove ineffective strbuf presizing that would have computed an
allocation that would not have fit in the available memory anyway,
or too small due to integer wraparound to cause immediate automatic
growing.
Will merge to 'master'.
source: <20260512162022.GA69669@coredump.intra.peff.net>
* kk/merge-octopus-optim (2026-05-11) 1 commit
(merged to 'next' on 2026-05-20 at afe427dc66)
+ merge: use repo_in_merge_bases for octopus up-to-date check
The logic to determine that branches in an octopus merge are
independent has been optimized.
Will merge to 'master'.
cf. <c5b333f1-0db6-4aec-a369-6503cb924e7f@gmail.com>
source: <pull.2110.git.1778566286543.gitgitgadget@gmail.com>
* rs/strbuf-add-uint (2026-05-12) 4 commits
- ls-tree: use strbuf_add_uint()
- ls-files: use strbuf_add_uint()
- cat-file: use strbuf_add_uint()
- strbuf: add strbuf_add_uint()
Adding a decimal integer with strbuf_addf("%u") appears commonly;
they have been optimized by using a custom formatter.
Comments?
source: <20260512115603.80780-1-l.s.r@web.de>
* ta/approxidate-noon-fix (2026-05-21) 4 commits
- approxidate: use deferred mday adjustments for "specials"
- approxidate: make "specials" respect fixed day-of-month
- t0006: add support for approxidate test date adjustment
- approxidate: make "today" wrap to midnight
"Friday noon" asked in the morning on Sunday was parsed to be one
day before the specified time, which has been corrected.
Will merge to 'next'.
source: <20260521105408.8222-1-taahol@utu.fi>
* mm/doc-word-diff (2026-05-13) 1 commit
- doc: clarify that --word-diff operates on line-level hunks
The documentation for "--word-diff" has been extended with a bit of
implementation detail of where these different words come from.
Comments?
source: <pull.2113.git.1778686956622.gitgitgadget@gmail.com>
* rs/strbuf-add-oid-hex (2026-05-13) 1 commit
- hex: add and use strbuf_add_oid_hex()
Formatting object name in full hexadecimal form has been optimized
by using a new strbuf_add_oid_hex() helper function.
Comments?
source: <183aa0fd-d455-4ec9-9c42-d511fac8b3e4@web.de>
* kk/limit-list-optim (2026-05-14) 1 commit
(merged to 'next' on 2026-05-19 at f17450dd1b)
+ revision: use priority queue in limit_list()
The limit_list() function that is one of the core part of the
revision traversal infrastructure has been optimized by replacing
its use of linear list with priority queue.
Will merge to 'master'.
source: <pull.2114.git.1778777491939.gitgitgadget@gmail.com>
* ed/check-connected-close-err-fd (2026-05-16) 1 commit
(merged to 'next' on 2026-05-22 at 00d592399e)
+ Merge branch 'ed/check-connected-close-err-fd-2.53' into ed/check-connected-close-err-fd
(this branch uses ed/check-connected-close-err-fd-2.53.)
File descriptor leak fix.
Will merge to 'master'.
(this branch uses ed/check-connected-close-err-fd-2.53.)
* ed/check-connected-close-err-fd-2.53 (2026-05-14) 1 commit
(merged to 'next' on 2026-05-22 at 1017d0e022)
+ connected: close err_fd in promisor fast-path
(this branch is used by ed/check-connected-close-err-fd.)
File descriptor leak fix (for 2.54 maintenance track).
Will merge to 'master'.
source: <pull.2303.git.git.1778827194448.gitgitgadget@gmail.com>
* kk/tips-reachable-from-bases-optim (2026-05-16) 2 commits
(merged to 'next' on 2026-05-22 at 87d6b8e666)
+ t6600: add tests for duplicate tips in tips_reachable_from_bases()
+ commit-reach: use object flags for tips_reachable_from_bases()
Revision traversal optimization.
Will merge to 'master'.
source: <pull.2116.v3.git.1778947182.gitgitgadget@gmail.com>
* pb/doc-diff-format-updates (2026-05-15) 3 commits
(merged to 'next' on 2026-05-20 at fe8d31e9f9)
+ diff-format.adoc: mode and hash are 0* for unmerged paths from index only
+ diff-format.adoc: 'git diff-files' prints two lines for unmerged files
+ diff-format.adoc: remove mention of diff-tree specific output
Doc updates.
Will merge to 'master'.
source: <pull.2304.git.git.1778860091.gitgitgadget@gmail.com>
* ps/t3903-cover-stash-include-untracked (2026-05-16) 1 commit
(merged to 'next' on 2026-05-20 at f1e7ac1cbd)
+ stash: add coverage for show --include-untracked
Test coverage has been added to "git stash --include-untracked".
Will merge to 'master'.
source: <20260516183347.4323-2-pushkarkumarsingh1970@gmail.com>
* rs/trailer-fold-optim (2026-05-15) 1 commit
(merged to 'next' on 2026-05-20 at 38c9fb15c2)
+ trailer: change strbuf in-place in unfold_value()
Code simplification.
Will merge to 'master'.
source: <816be07e-2cd6-48fe-ae93-57fa0f2543ed@web.de>
* rs/use-builtin-add-overflow-explicitly-on-clang (2026-05-18) 2 commits
(merged to 'next' on 2026-05-21 at c223b71079)
+ use __builtin_add_overflow() in st_add() with Clang
+ strbuf: use st_add3() in strbuf_grow()
Micro optimization of codepaths that compute allocation sizes carefully.
Will merge to 'master'.
source: <20260518202502.25682-1-l.s.r@web.de>
* tc/generate-configlist-fix-for-older-ninja (2026-05-15) 1 commit
(merged to 'next' on 2026-05-22 at 8322bfb8f2)
+ generate-configlist: collapse depfile for older Ninja
Build update.
Will merge to 'master'.
source: <20260515-toon-fix-almalinux8-v3-1-b545a0647f0f@iotcl.com>
* hn/config-typo-advice (2026-05-16) 1 commit
- config: suggest the correct form when key contains "=" in set context
"git config foo.bar=baz" is not likely to be a request to read the
value of such a variable with '=' in its name; rather it is plausible
that the user meant "git config set foo.bar baz". Give advice when
giving an error message.
Comments?
source: <pull.2302.v2.git.git.1778935976330.gitgitgadget@gmail.com>
* ja/doc-synopsis-style-again (2026-05-17) 5 commits
- doc: convert git-imap-send synopsis and options to new style
- doc: convert git-apply synopsis and options to new style
- doc: convert git-am synopsis and options to new style
- doc: convert git-grep synopsis and options to new style
- doc: convert git-bisect to synopsis style
A batch of documentation pages has been updated to use the modern
synopsis style.
Comments?
source: <pull.2117.git.1779049615.gitgitgadget@gmail.com>
* kn/refs-fsck-skip-lock-files (2026-05-17) 1 commit
(merged to 'next' on 2026-05-21 at 91e30e3543)
+ refs/files: skip lock files during consistency checks
The consistency checks for the files reference backend have been updated
to skip lock files earlier, avoiding unnecessary parsing of
intermediate files.
Will merge to 'master'.
source: <20260517-refs-fsck-skip-lock-files-v3-1-b24dfd673c7e@gmail.com>
* jt/config-lock-timeout (2026-05-17) 1 commit
- config: retry acquiring config.lock, configurable via core.configLockTimeout
Configuration file locking now retries for a short period, avoiding
failures when multiple processes attempt to update the configuration
simultaneously.
Comments?
cf. <xmqqzf1xbl4i.fsf@gitster.g>
source: <20260517132111.1014901-1-joerg@thalheim.io>
* ag/sequencer-remove-unused-struct-member (2026-05-11) 1 commit
(merged to 'next' on 2026-05-17 at 8553437ae1)
+ sequencer: remove todo_add_branch_context.commit
Code clean-up.
Will merge to 'master'.
cf. <agLKVn6RF4UBYd_8@pks.im>
source: <pull.2111.git.1778502113485.gitgitgadget@gmail.com>
* hn/branch-prune-merged (2026-05-21) 4 commits
- branch: add --dry-run for --prune-merged
- branch: add branch.<name>.pruneMerged opt-out
- branch: add --prune-merged <branch>
- branch: add --forked <branch>
"git branch" command learned "--prune-merged" option to remove
local branches that have already been merged to the remote-tracking
branches they track.
Comments?
source: <pull.2285.v10.git.git.1779403204.gitgitgadget@gmail.com>
* mm/diff-U-takes-no-negative-values (2026-05-12) 4 commits
(merged to 'next' on 2026-05-17 at d81439a049)
+ parse-options: clarify what "negated" means for PARSE_OPT_NONEG
+ xdiff: guard against negative context lengths
+ diff: reject negative values for -U/--unified
+ diff: reject negative values for --inter-hunk-context
The command line parser for "git diff" learned a few options take
only non-negative integers.
Will merge to 'master'.
source: <pull.2105.v2.git.1778609423.gitgitgadget@gmail.com>
* dk/doc-exclude-is-shared-per-repo (2026-05-12) 1 commit
(merged to 'next' on 2026-05-17 at ddc761aec6)
+ ignore: note info/exclude lives in GIT_COMMON_DIR, not GIT_DIR
Document the fact that .git/info/exclude is shared across worktrees
linked to the same repository.
Will merge to 'master'.
cf. <bea48414-217b-4860-9279-fe94e3687c28@gmail.com>
source: <ec97ad3f054e90b675f099a36a81a23bb4b2a0ed.1778620784.git.ben.knoble+github@gmail.com>
* kk/paint-down-to-common-optim (2026-05-11) 2 commits
(merged to 'next' on 2026-05-17 at 2e39c767e5)
+ commit-reach: early exit paint_down_to_common for single merge-base
+ commit-reach: introduce merge_base_flags enum
"git merge-base" optimization.
Will merge to 'master'.
source: <pull.2109.v4.git.1778504352.gitgitgadget@gmail.com>
* st/daemon-sockaddr-fixes (2026-05-14) 3 commits
- daemon: guard NULL REMOTE_PORT in execute() logging
- daemon: fix IPv6 address truncation in ip2str()
- daemon: fix IPv6 address corruption in lookup_hostname()
Correct use of sockaddr API in "git daemon".
Waiting for response(s) to review comment(s).
cf. <agGLRC1ziF5F8Okh@pks.im>
source: <pull.2300.git.git.1778773592.gitgitgadget@gmail.com>
* ob/more-repo-config-values (2026-04-23) 8 commits
- env: move "warn_on_object_refname_ambiguity" into `struct repo_config_values`
- env: move "sparse_expect_files_outside_of_patterns" into `repo_config_values`
- env: move "core_sparse_checkout_cone" into `struct repo_config_values`
- environment: move "precomposed_unicode" into `struct repo_config_values`
- environment: move "pack_compression_level" into `struct repo_config_values`
- environment: move `zlib_compression_level` into `struct repo_config_values`
- environment: move "check_stat" into `struct repo_config_values`
- environment: move "trust_ctime" into `struct repo_config_values`
Expecting a reroll.
cf. <CAD=f0L8-_3sDGGkCzF4WA0xmUtaY_qiz__3zq5AemLgwTsqvsg@mail.gmail.com>
cf. <xmqqlddqu013.fsf@gitster.g>
source: <20260423165432.143598-1-belkid98@gmail.com>
* cc/promisor-auto-config-url-more (2026-05-19) 9 commits
- doc: promisor: improve acceptFromServer entry
- promisor-remote: auto-configure unknown remotes
- promisor-remote: trust known remotes matching acceptFromServerUrl
- promisor-remote: introduce promisor.acceptFromServerUrl
- promisor-remote: add 'local_name' to 'struct promisor_info'
- urlmatch: add url_normalize_pattern() helper
- urlmatch: change 'allow_globs' arg to bool
- t5710: simplify 'mkdir X' followed by 'git -C X init'
- Merge branch 'cc/promisor-auto-config-url' into cc/promisor-auto-config-url-more
The handling of promisor-remote protocol capability has been
loosened to allow the other side to add to the list of promisor
remotes via the promisor.acceptFromServerURL configuration
variable.
Comments?
source: <20260519153808.494105-1-christian.couder@gmail.com>
* hn/checkout-track-fetch (2026-05-21) 1 commit
- checkout: extend --track with a "fetch" mode to refresh start-point
"git checkout --track=..." learned to optionally fetch the branch
from the remote the new branch will work with.
Comments?
source: <pull.2281.v12.git.git.1779358803652.gitgitgadget@gmail.com>
* mf/revision-max-count-oldest (2026-05-18) 1 commit
- revision.c: implement --max-count-oldest
"git rev-list" (and "git log" family of commands) learned a new "--max-count-oldest"
that picks oldest N commits in the range instead of the usual newest.
Comments?
source: <8210d60832b9a58aa4d71fc3790e44d8989564ce.1779152064.git.mroik@delayed.space>
* mm/line-log-cleanup (2026-04-27) 3 commits
- line-log: allow non-patch diff formats with -L
- line-log: integrate -L output with the standard log-tree pipeline
- revision: move -L setup before output_format-to-diff derivation
Code clean-up.
Comments?
cf. <xmqqfr3xp98b.fsf@gitster.g>
source: <pull.2094.git.1777349126.gitgitgadget@gmail.com>
* ds/path-walk-filters (2026-05-13) 14 commits
- path-walk: support `combine` filter
- path-walk: support `object:type` filter
- path-walk: support `tree:0` filter
- t6601: tag otherwise-unreachable trees
- pack-objects: support sparse:oid filter with path-walk
- path-walk: add pl_sparse_trees to control tree pruning
- path-walk: support blob size limit filter
- backfill: die on incompatible filter options
- path-walk: support blobless filter
- path-walk: always emit directly-requested objects
- t/perf: add pack-objects filter and path-walk benchmark
- pack-objects: pass --objects with --path-walk
- t5620: make test work with path-walk var
- Merge branch 'en/backfill-fixes-and-edges' into ds/path-walk-filters
The "git pack-objects --path-walk" traversal has been integrated
with several object filters, including blobless and sparse filters.
Comments?
source: <pull.2101.v4.git.1778707135.gitgitgadget@gmail.com>
* en/ort-harden-against-corrupt-trees (2026-04-20) 5 commits
- cache-tree: fix verify_cache() to catch non-adjacent D/F conflicts
- merge-ort: abort merge when trees have duplicate entries
- merge-ort: free diff pairs queue in clear_or_reinit_internal_opts()
- merge-ort: drop unnecessary show_all_errors from collect_merge_info()
- merge-ort: propagate callback errors from traverse_trees_wrapper()
"ort" merge backend handles merging corrupt trees better by
aborting when it should.
Needs review.
source: <pull.2096.git.1776731171.gitgitgadget@gmail.com>
* pw/status-rebase-todo (2026-05-01) 2 commits
- status: improve rebase todo list parsing
- sequencer: factor out parsing of todo commands
The display of the rebase todo list in "git status" has been
improved to correctly abbreviate object IDs for more commands and
avoid misinterpreting refs as object IDs.
Needs review.
source: <cover.1777648598.git.phillip.wood@dunelm.org.uk>
* tb/pseudo-merge-bugfixes (2026-05-11) 9 commits
(merged to 'next' on 2026-05-19 at ecee155d5c)
+ pack-bitmap: prevent pattern leak on pseudo-merge re-assignment
+ Documentation: fix broken `sampleRate` in gitpacking(7)
+ pack-bitmap: reject pseudo-merge "sampleRate" of 0
+ pack-bitmap: parse commits in `find_pseudo_merge_group_for_ref()`
+ pack-bitmap: fix pseudo-merge lookup for shared commits
+ pack-bitmap: fix inverted binary search in `pseudo_merge_at()`
+ pack-bitmap-write: sort pseudo-merge commit lookup table in pack order
+ t5333: demonstrate various pseudo-merge bugs
+ t/helper: add 'test-tool bitmap write' subcommand
(this branch is used by tb/bitmap-build-performance.)
Fixes many bugs in pseudo-merge code.
Will merge to 'master'.
source: <cover.1778546804.git.me@ttaylorr.com>
* ds/fetch-negotiation-options (2026-05-19) 8 commits
(merged to 'next' on 2026-05-21 at ff57fd9c97)
+ send-pack: pass negotiation config in push
+ remote: add remote.*.negotiationInclude config
+ fetch: add --negotiation-include option for negotiation
+ negotiator: add have_sent() interface
+ remote: add remote.*.negotiationRestrict config
+ transport: rename negotiation_tips
+ fetch: add --negotiation-restrict option
+ t5516: fix test order flakiness
The negotiation tip options in "git fetch" have been reworked to
allow requiring certain refs to be sent as "have" lines, and to
restrict negotiation to a specific set of refs.
Will merge to 'master'.
source: <pull.2085.v6.git.1779207896.gitgitgadget@gmail.com>
* en/batch-prefetch (2026-05-14) 4 commits
(merged to 'next' on 2026-05-20 at 722acf81c8)
+ grep: prefetch necessary blobs
+ builtin/log: prefetch necessary blobs for `git cherry`
+ patch-ids.h: add missing trailing parenthesis in documentation comment
+ promisor-remote: document caller filtering contract
In a lazy clone, "git cherry" and "git grep" often fetch necessary
blob objects one by one from promisor remotes. It has been corrected
to collect necessary object names and fetch them in bulk to gain
reasonable performance.
Will merge to 'master'.
cf. <0da4f159-8d4b-49e2-93c1-25aa0bf69371@gmail.com>
source: <pull.2089.v3.git.1778775928.gitgitgadget@gmail.com>
* ps/odb-in-memory (2026-04-10) 18 commits
(merged to 'next' on 2026-05-21 at c8709aa17f)
+ t/unit-tests: add tests for the in-memory object source
+ odb: generic in-memory source
+ odb/source-inmemory: stub out remaining functions
+ odb/source-inmemory: implement `freshen_object()` callback
+ odb/source-inmemory: implement `count_objects()` callback
+ odb/source-inmemory: implement `find_abbrev_len()` callback
+ odb/source-inmemory: implement `for_each_object()` callback
+ odb/source-inmemory: convert to use oidtree
+ oidtree: add ability to store data
+ cbtree: allow using arbitrary wrapper structures for nodes
+ odb/source-inmemory: implement `write_object_stream()` callback
+ odb/source-inmemory: implement `write_object()` callback
+ odb/source-inmemory: implement `read_object_stream()` callback
+ odb/source-inmemory: implement `read_object_info()` callback
+ odb: fix unnecessary call to `find_cached_object()`
+ odb/source-inmemory: implement `free()` callback
+ odb: introduce "in-memory" source
+ Merge branch 'jt/odb-transaction-write' into ps/odb-in-memory
(this branch is used by ps/odb-source-loose; uses jt/odb-transaction-write.)
Add a new odb "in-memory" source that is meant to only hold
tentative objects (like the virtual blob object that represents the
working tree file used by "git blame").
Will merge to 'master'.
source: <20260410-b4-pks-odb-source-inmemory-v3-0-22fd0fad58fe@pks.im>
* cl/conditional-config-on-worktree-path (2026-05-13) 2 commits
(merged to 'next' on 2026-05-22 at 7851f494ae)
+ config: add "worktree" and "worktree/i" includeIf conditions
+ config: refactor include_by_gitdir() into include_by_path()
The [includeIf "condition"] conditional inclusion facility for
configuration files has learned to use the location of worktree
in its condition.
Will merge to 'master'.
cf. <2989eb07-2933-4b5a-9e5c-33ef9b805528@gmail.com>
source: <20260513-includeif-worktree-v4-0-f8e6212d1fba@black-desk.cn>
* ps/shift-root-in-graph (2026-04-27) 1 commit
- graph: add indentation for commits preceded by a parentless commit
In a history with more than one root commit, "git log --graph
--oneline" stuffed an unrelated commit immediately below a root
commit, which has been corrected by making the spot below a root
unavailable.
Waiting for response(s) to review comment(s).
cf. <20260513230216.GA1378627@coredump.intra.peff.net>
source: <20260427102838.44867-2-pabloosabaterr@gmail.com>
* lp/repack-propagate-promisor-debugging-info (2026-04-18) 6 commits
- repack-promisor: add missing headers
- t7703: test for promisor file content after geometric repack
- t7700: test for promisor file content after repack
- repack-promisor: preserve content of promisor files after repack
- repack-promisor add helper to fill promisor file after repack
- pack-write: add explanation to promisor file content
When fetching objects into a lazily cloned repository, .promisor
files are created with information meant to help debugging. "git
repack" has been taught to carry this information forward to
packfiles that are newly created.
Needs review.
cf. <xmqqse7xm8av.fsf@gitster.g>
source: <cover.1776384902.git.lorenzo.pegorari2002@gmail.com>
* th/promisor-quiet-per-repo (2026-04-06) 1 commit
- promisor-remote: fix promisor.quiet to use the correct repository
The "promisor.quiet" configuration variable was not used from
relevant submodules when commands like "grep --recurse-submodules"
triggered a lazy fetch, which has been corrected.
Comments?
source: <20260406183041.783800-1-vikingtc4@gmail.com>
* jt/odb-transaction-write (2026-05-14) 7 commits
(merged to 'next' on 2026-05-21 at 61108abe4d)
+ odb/transaction: make `write_object_stream()` pluggable
+ object-file: generalize packfile writes to use odb_write_stream
+ object-file: avoid fd seekback by checking object size upfront
+ object-file: remove flags from transaction packfile writes
+ odb: update `struct odb_write_stream` read() callback
+ odb/transaction: use pluggable `begin_transaction()`
+ odb: split `struct odb_transaction` into separate header
(this branch is used by ps/odb-in-memory and ps/odb-source-loose.)
ODB transaction interface is being reworked to explicitly handle
object writes.
Will merge to 'master'.
source: <20260514183740.1505171-1-jltobler@gmail.com>
* sa/cat-file-batch-mailmap-switch (2026-04-15) 1 commit
(merged to 'next' on 2026-05-22 at 197a9bad73)
+ cat-file: add mailmap subcommand to --batch-command
"git cat-file --batch" learns an in-line command "mailmap"
that lets the user toggle use of mailmap.
Will merge to 'master'.
cf. <xmqqwlwy4v7t.fsf@gitster.g>
source: <20260416033250.4327-2-siddharthasthana31@gmail.com>
* tb/incremental-midx-part-3.3 (2026-05-19) 16 commits
(merged to 'next' on 2026-05-21 at 6c11c1a739)
+ repack: allow `--write-midx=incremental` without `--geometric`
+ repack: introduce `--write-midx=incremental`
+ repack: implement incremental MIDX repacking
+ packfile: ensure `close_pack_revindex()` frees in-memory revindex
+ builtin/repack.c: convert `--write-midx` to an `OPT_CALLBACK`
+ repack-geometry: prepare for incremental MIDX repacking
+ repack-midx: extract `repack_fill_midx_stdin_packs()`
+ repack-midx: factor out `repack_prepare_midx_command()`
+ midx: expose `midx_layer_contains_pack()`
+ repack: track the ODB source via existing_packs
+ midx: support custom `--base` for incremental MIDX writes
+ midx: introduce `--no-write-chain-file` for incremental MIDX writes
+ midx: use `strvec` for `keep_hashes`
+ midx: build `keep_hashes` array in order
+ midx: use `strset` for retained MIDX files
+ midx-write: handle noop writes when converting incremental chains
The repacking code has been refactored and compaction of MIDX layers
have been implemented, and incremental strategy that does not require
all-into-one repacking has been introduced.
Will merge to 'master'.
source: <cover.1779206239.git.me@ttaylorr.com>
* jd/unpack-trees-wo-the-repository (2026-03-31) 2 commits
- unpack-trees: use repository from index instead of global
- unpack-trees: use repository from index instead of global
A handful of inappropriate uses of the_repository have been
rewritten to use the right repository structure instance in the
unpack-trees.c codepath.
Comments?
source: <pull.2258.v2.git.git.1774971267.gitgitgadget@gmail.com>
* ps/setup-wo-the-repository (2026-05-19) 18 commits
(merged to 'next' on 2026-05-21 at d8fb5a7b3e)
+ setup: stop using `the_repository` in `init_db()`
+ setup: stop using `the_repository` in `create_reference_database()`
+ setup: stop using `the_repository` in `initialize_repository_version()`
+ setup: stop using `the_repository` in `check_repository_format()`
+ setup: stop using `the_repository` in `upgrade_repository_format()`
+ setup: stop using `the_repository` in `setup_git_directory()`
+ setup: stop using `the_repository` in `setup_git_directory_gently()`
+ setup: stop using `the_repository` in `setup_git_env()`
+ setup: stop using `the_repository` in `set_git_work_tree()`
+ setup: stop using `the_repository` in `setup_work_tree()`
+ setup: stop using `the_repository` in `enter_repo()`
+ setup: stop using `the_repository` in `verify_non_filename()`
+ setup: stop using `the_repository` in `verify_filename()`
+ setup: stop using `the_repository` in `path_inside_repo()`
+ setup: stop using `the_repository` in `prefix_path()`
+ setup: stop using `the_repository` in `is_inside_work_tree()`
+ setup: stop using `the_repository` in `is_inside_git_dir()`
+ setup: replace use of `the_repository` in static functions
(this branch is used by ps/setup-centralize-odb-creation.)
Many uses of the_repository has been updated to use a more
appropriate struct repository instance in setup.c codepath.
Will merge to 'master'.
source: <20260519-pks-setup-wo-the-repository-v3-0-a00d8ea8b07f@pks.im>
* kh/doc-trailers (2026-04-13) 9 commits
- doc: interpret-trailers: document comment line treatment
- doc: interpret-trailers: commit to “trailer block” term
- doc: interpret-trailers: add key format example
- doc: interpret-trailers: explain key format
- doc: interpret-trailers: explain the format after the intro
- doc: interpret-trailers: not just for commit messages
- doc: interpret-trailers: use “metadata” in Name as well
- doc: interpret-trailers: replace “lines” with “metadata”
- doc: interpret-trailers: stop fixating on RFC 822
Documentation updates.
Needs review.
cf. <xmqq1pfivfa3.fsf@gitster.g>
source: <V2_CV_doc_int-tr_key_format.613@msgid.xyz>
* ps/graph-lane-limit (2026-03-27) 3 commits
(merged to 'next' on 2026-05-22 at ca1c5e8432)
+ graph: add truncation mark to capped lanes
+ graph: add --graph-lane-limit option
+ graph: limit the graph width to a hard-coded max
The graph output from commands like "git log --graph" can now be
limited to a specified number of lanes, preventing overly wide output
in repositories with many branches.
Will merge to 'master'.
cf. <bdff0a5d-b738-4053-9b72-08eba88156de@kdbg.org>
source: <20260328001113.1275291-1-pabloosabaterr@gmail.com>
* jr/bisect-custom-terms-in-output (2026-05-14) 3 commits
(merged to 'next' on 2026-05-22 at 1ccd1056c9)
+ rev-parse: use selected alternate terms to look up refs
+ bisect: print bisect terms in single quotes
+ bisect: use selected alternate terms in status output
"git bisect" now uses the selected terms (e.g., old/new) more
consistently in its output.
Will merge to 'master'.
source: <20260514-bisect-terms-v4-0-b3e3cf1b06ce@schlaraffenlan.de>
* ua/push-remote-group (2026-05-03) 3 commits
- push: support pushing to a remote group
- remote: move remote group resolution to remote.c
- remote: fix sign-compare warnings in push_cas_option
"git push" learned to take a "remote group" name to push to, which
causes pushes to multiple places, just like "git fetch" would do.
Comments?
source: <20260503153402.1333220-1-usmanakinyemi202@gmail.com>
* js/parseopt-subcommand-autocorrection (2026-04-27) 11 commits
- SQUASH???
- doc: document autocorrect API
- parseopt: add tests for subcommand autocorrection
- parseopt: enable subcommand autocorrection for git-remote and git-notes
- parseopt: autocorrect mistyped subcommands
- autocorrect: provide config resolution API
- autocorrect: rename AUTOCORRECT_SHOW to AUTOCORRECT_HINT
- autocorrect: use mode and delay instead of magic numbers
- help: move tty check for autocorrection to autocorrect.c
- help: make autocorrect handling reusable
- parseopt: extract subcommand handling from parse_options_step()
The parse-options library learned to auto-correct misspelled
subcommand names.
Expecting a reroll.
cf. <xmqqcxz2tzpr.fsf@gitster.g>
source: <SY0P300MB0801677A2A1E0FD38D06A841CE2A2@SY0P300MB0801.AUSP300.PROD.OUTLOOK.COM>
* jc/neuter-sideband-post-3.0 (2026-03-05) 2 commits
- sideband: delay sanitizing by default to Git v3.0
- Merge branch 'jc/neuter-sideband-fixup' into jc/neuter-sideband-post-3.0
The final step, split from earlier attempt by Dscho, to loosen the
sideband restriction for now and tighten later at Git v3.0 boundary.
On hold to help the base topic with wider exposure.
(this branch uses jc/neuter-sideband-fixup.)
source: <20260305233452.3727126-8-gitster@pobox.com>
* cs/subtree-split-recursion (2026-03-05) 3 commits
- contrib/subtree: reduce recursion during split
- contrib/subtree: functionalize split traversal
- contrib/subtree: reduce function side-effects
When processing large history graphs on Debian or Ubuntu, "git
subtree" can die with a "recursion depth reached" error.
Comments?
source: <20260305-cs-subtree-split-recursion-v2-0-7266be870ba9@howdoi.land>
* pt/fsmonitor-linux (2026-04-15) 13 commits
(merged to 'next' on 2026-05-22 at 5d99c1765d)
+ fsmonitor: convert shown khash to strset in do_handle_client
+ fsmonitor: add tests for Linux
+ fsmonitor: add timeout to daemon stop command
+ fsmonitor: close inherited file descriptors and detach in daemon
+ run-command: add close_fd_above_stderr option
+ fsmonitor: implement filesystem change listener for Linux
+ fsmonitor: rename fsm-settings-darwin.c to fsm-settings-unix.c
+ fsmonitor: rename fsm-ipc-darwin.c to fsm-ipc-unix.c
+ fsmonitor: use pthread_cond_timedwait for cookie wait
+ compat/win32: add pthread_cond_timedwait
+ fsmonitor: fix hashmap memory leak in fsmonitor_run_daemon
+ fsmonitor: fix khash memory leak in do_handle_client
+ t9210, t9211: disable GIT_TEST_SPLIT_INDEX for scalar clone tests
The fsmonitor daemon has been implemented for Linux.
Will merge to 'master'.
cf. <xmqqa4u5nnxq.fsf@gitster.g>
source: <pull.2147.v15.git.git.1776259657.gitgitgadget@gmail.com>
^ permalink raw reply
* Re: [PATCH v2 01/11] git-gui: guard set/unset of GIT_DIR and GIT_WORK_TREE
From: Johannes Sixt @ 2026-05-23 8:18 UTC (permalink / raw)
To: Mark Levedahl; +Cc: egg_mushroomcow, bootaina702, git
In-Reply-To: <eb748327-6652-4477-82bb-9db9f8388ec0@gmail.com>
Am 22.05.26 um 13:54 schrieb Mark Levedahl:
> The manual page is incomplete: if the repository has set core.worktree=/somehere, that is
> the root of the worktree and the current directory is always ignored. git rev-parse will
> report /somewhere as the answer to --show-toplevel regardless of current directory, even
> if inside the gitdir, and even if GIT_DIR is used.
>
> The user can override with GIT_WORK_TREE, and if so we must keep GIT_WORK_TREE in the
> environment if it was set. [...]
Oh, well, these intricacies! Let's scrap my patch and keep yours.
The other patch that removes cd $_gitworktree from do_gitk should still
be good, I think.
-- Hannes
^ permalink raw reply
* Re: [PATCH v2 07/11] git-gui: try harder to find worktree from gitdir
From: Johannes Sixt @ 2026-05-23 8:01 UTC (permalink / raw)
To: Shroom Moo; +Cc: git, Aina Boot, Mark Levedahl
In-Reply-To: <tencent_E13EB585242AD7C263B8B3B732A428465D09@qq.com>
Am 21.05.26 um 06:55 schrieb Shroom Moo:
> On 5/21/26 4:24 AM, Mark Levedahl wrote:
>> + } elseif [file exists {gitdir}] {
>> + if {[catch {
>> + set fd_gitdir [open {gitdir} {r}]
>> + set gitlink_parent [file dirname [read $fd_gitdir]]
>> + catch {close $fd_gitdir}
>> + set worktree [git -C $gitlink_parent rev-parse --show-toplevel]
>> + set parent_gitdir [git -C $worktree rev-parse --absolute-git-dir]
>> + if {$::_gitdir ne $parent_gitdir} {
>> + set worktree {}
>> + }
>> + }]} {
>> + catch {close $fd_gitdir}
>> + set worktree {}
>> + }
>> + }
> Additionally, [file exists {gitdir}] checks for the gitdir file in
> the current working directory. Since the function has not yet
> switched to $_gitdir when this check runs, it is almost impossible
> to find the file. Consequently, this logic never triggers, preventing
> linked worktrees from being recognized.
I think you are misunderstanding which use-case this code is addressing.
The case can be triggered very easily.
First, the code before the part we see above is intended for the special
case where we start in a .git, where `--show-toplevel` bails out and we
define the worktree to be the directory containing .git.
However, if we start in .git/worktrees/feature, then the code cited
above kicks in, because `--show-toplevel` still bails out,
`--absolute-git-dir` does not end in '.git', but now we have a file
named 'gitdir' in the current directory. In this case, we define (and
this is new with this patch) that the worktree is the one where the
'gitdir' points.
-- Hannes
^ permalink raw reply
* [PATCH] log: improve --follow following renames in merge commits
From: Miklos Vajna @ 2026-05-23 6:04 UTC (permalink / raw)
To: Jeff King; +Cc: Elijah Newren, git
In-Reply-To: <20260522054312.GD861761@coredump.intra.peff.net>
Have a repo with a subtree merge, do a 'git log --follow prefix/test.c',
the output only contains history in the outer repo, not commits that
were merged via a subtree merge.
There is an inherent limitation of the current 'git log --follow'
design, since it's limited to a single filename, and once 'git log' sees
a rename, it only tracks the new path, which only works with mostly
linear history. Still, 'git blame prefix/test.c' does find the original
commits, so it's fair to expect 'git log --follow' can do the same.
Fix the problem by improving when to update the followed path in
log_tree_diff(). If the path is untouched versus the merge result in all
parents but one, then choose the parent where it was changed, including
any --follow processing.
This is almost the same as requiring that all but one parents are
TREESAME, except we don't consider the addition of a file as
"interesting". With this, the pre-merge history of subtree merge is
visible in git log, but the behavior is unchanged for other cases (e.g.
when a file was previously named differently on multiple parents).
Signed-off-by: Miklos Vajna <vmiklos@collabora.com>
---
Hi Jeff,
On Fri, May 22, 2026 at 01:43:12AM -0400, Jeff King <peff@peff.net> wrote:
> I think we can probably all agree that both before and after your patch,
> --follow is never going to do the _right_ thing, which is to follow
> paths independently down both sides of history.
Sure.
> I am OK conceptually with making the current broken behavior slightly
> more useful if it is easy to do. But I am not sure if we are making
> things more useful here or not. If we see a merge where the file "bar"
> was previous "foo" on one side and "bar" on the other, our broken follow
> is going to either pick "foo" or "bar" to continue with as we traverse.
> But which one is right? Whichever name we choose, we are potentially
> omitting results from the other side.
Indeed, I didn't consider this case.
> There might be a more useful rule like: if the path is untouched versus
> the merge result in all parents but one (i.e., TREESAME), then choose
> the parent where it was changed, including any --follow processing.
I like this idea: it keeps working with the subtree use-case I have in
mind and goes back to not change behavior when the file has history on
multiple parents.
> So I dunno. Probably some experimenting could yield more analysis there,
I think requiring TREESAME for all but one parents is too strict, since
a subtree merge will look like an addition vs the first parent and will
look like a rename on the first parent. It seems to me that handling
addition as TREESAME can be correct: if the file was just added, that
suggests it has no prior history.
So a slightly relaxed rule could be: if the path is untouched or just
added versus the merge result in all parents but one, then choose the
parent where it was changed, including any --follow processing.
Here is a patch that implements that idea. It works for the subtree
merge use-case I outlined and I also added a test to show that the
behavior is unchanged for the "multiple parents have actual history for
this file" case you mentioned.
What do you think?
Thanks,
Miklos
log-tree.c | 55 ++++++++++++++++++++++++-
t/meson.build | 1 +
t/t4218-log-follow-subtree-merge.sh | 64 +++++++++++++++++++++++++++++
3 files changed, 119 insertions(+), 1 deletion(-)
create mode 100755 t/t4218-log-follow-subtree-merge.sh
diff --git a/log-tree.c b/log-tree.c
index 7e048701d0..368144fafc 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -1142,8 +1142,61 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log
/* Show parent info for multiple diffs */
log->parent = parents->item;
}
- } else
+ } else {
+ if (opt->diffopt.flags.follow_renames) {
+ /*
+ * If the path is untouched in all parents but
+ * one, then choose the parent where it was
+ * changed.
+ */
+ struct commit_list *p;
+ struct commit *changed_parent = NULL;
+ int n_changed = 0;
+
+ for (p = parents; p; p = p->next) {
+ struct diff_options diff_opts;
+ int interesting = 0;
+ int i;
+
+ parse_commit_or_die(p->item);
+ repo_diff_setup(opt->diffopt.repo, &diff_opts);
+ copy_pathspec(&diff_opts.pathspec,
+ &opt->diffopt.pathspec);
+ diff_opts.flags.recursive = 1;
+ diff_opts.flags.follow_renames = 1;
+ diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+ diff_setup_done(&diff_opts);
+ diff_tree_oid(get_commit_tree_oid(p->item),
+ oid, "", &diff_opts);
+
+ for (i = 0; i < diff_queued_diff.nr; i++) {
+ struct diff_filepair *pair = diff_queued_diff.queue[i];
+ if (DIFF_FILE_VALID(pair->one)) {
+ interesting = 1;
+ break;
+ }
+ }
+
+ diff_queue_clear(&diff_queued_diff);
+ diff_free(&diff_opts);
+
+ if (interesting) {
+ n_changed++;
+ changed_parent = p->item;
+ if (n_changed > 1)
+ break;
+ }
+ }
+
+ if (n_changed == 1) {
+ diff_tree_oid(get_commit_tree_oid(changed_parent),
+ oid, "", &opt->diffopt);
+ diff_queue_clear(&diff_queued_diff);
+ opt->diffopt.found_follow = 0;
+ }
+ }
return 0;
+ }
}
showed_log = 0;
diff --git a/t/meson.build b/t/meson.build
index 7528e5cda5..b4ae8d76d8 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -574,6 +574,7 @@ integration_tests = [
't4215-log-skewed-merges.sh',
't4216-log-bloom.sh',
't4217-log-limit.sh',
+ 't4218-log-follow-subtree-merge.sh',
't4252-am-options.sh',
't4253-am-keep-cr-dos.sh',
't4254-am-corrupt.sh',
diff --git a/t/t4218-log-follow-subtree-merge.sh b/t/t4218-log-follow-subtree-merge.sh
new file mode 100755
index 0000000000..fc846ebeb4
--- /dev/null
+++ b/t/t4218-log-follow-subtree-merge.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+test_description='Test --follow follows renames across subtree merges'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'setup subtree-merged repository' '
+ git init inner &&
+ echo inner >inner/inner.txt &&
+ git -C inner add inner.txt &&
+ git -C inner commit -m "inner init" &&
+
+ git init outer &&
+ echo outer >outer/outer.txt &&
+ git -C outer add outer.txt &&
+ git -C outer commit -m "outer init" &&
+
+ git -C outer fetch ../inner master &&
+ git -C outer merge -s ours --no-commit --allow-unrelated-histories \
+ FETCH_HEAD &&
+ git -C outer read-tree --prefix=inner/ -u FETCH_HEAD &&
+ git -C outer commit -m "Merge inner repo into inner/ subdirectory"
+'
+
+test_expect_success '--follow finds the pre-merge commit through a subtree merge' '
+ git -C outer log --follow --pretty=tformat:%s inner/inner.txt >actual &&
+ echo "inner init" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'setup merge with rename sources on multiple parents' '
+ git init left &&
+ printf "shared content\n" >left/a.txt &&
+ git -C left add a.txt &&
+ git -C left commit -m "left: a.txt" &&
+
+ git init right &&
+ printf "shared content\n" >right/b.txt &&
+ git -C right add b.txt &&
+ git -C right commit -m "right: b.txt" &&
+
+ git -C left fetch ../right master &&
+ git -C left merge -s ours --no-commit --allow-unrelated-histories \
+ FETCH_HEAD &&
+ git -C left rm a.txt &&
+ printf "shared content\n" >left/c.txt &&
+ git -C left add c.txt &&
+ git -C left commit -m "Merge: rename to c.txt" &&
+
+ printf "more content\n" >>left/c.txt &&
+ git -C left add c.txt &&
+ git -C left commit -m "modify c.txt"
+'
+
+test_expect_success '--follow does not switch when multiple parents supply a rename source' '
+ git -C left log --follow --pretty=tformat:%s c.txt >actual &&
+ echo "modify c.txt" >expect &&
+ test_cmp expect actual
+'
+
+test_done
--
2.51.0
^ permalink raw reply related
* [PATCH v2] stash: reuse cached index entries in --patch temporary index
From: Adam Johnson via GitGitGadget @ 2026-05-22 23:12 UTC (permalink / raw)
To: git
Cc: Thomas Gummerer, Elijah Newren, Phillip Wood, Victoria Dye,
Adam Johnson, Adam Johnson
In-Reply-To: <pull.2306.git.git.1779194605735.gitgitgadget@gmail.com>
From: Adam Johnson <me@adamj.eu>
`git stash -p` prepares the interactive selection by creating a
temporary index at HEAD, switching `GIT_INDEX_FILE` to it, and then
running the `add -p` machinery.
That temporary index was created by running `git read-tree HEAD`. The
resulting index had no useful cached stat data or fsmonitor-valid bits
from the real index. When `run_add_p()` refreshed that temporary index
before showing the first prompt, it could end up lstat(2)-ing every
tracked file, even in a repository where `git diff` and `git restore -p`
can use fsmonitor to avoid that work.
Create the temporary index in-process instead. Use `unpack_trees()` to
reset the real index contents to HEAD while writing the result to the
temporary index path. For paths whose index entries already match HEAD,
`oneway_merge()` reuses the existing cache entries, preserving their
cached stat data and `CE_FSMONITOR_VALID` state.
This makes the refresh performed by `run_add_p()` behave like the one
used by `git restore -p`: unchanged paths can be skipped via fsmonitor
instead of being scanned again.
In a 206k file repository with `core.fsmonitor` enabled and a one-line
change in one file, time to first prompt dropped from 34.774 seconds to
0.659 seconds. The new perf test file demonstrates similar improvements,
with maen times for without- and with-fsmonitor cases dropping from 6.90
and 6.83 seconds to 0.55 and 0.28 seconds, respectively.
Signed-off-by: Adam Johnson <me@adamj.eu>
---
stash: reuse cached index entries in --patch temporary index
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2306%2Fadamchainz%2Faj%2Foptimize-stash-patch-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2306/adamchainz/aj/optimize-stash-patch-v2
Pull-Request: https://github.com/git/git/pull/2306
Range-diff vs v1:
1: b228160cc4 ! 1: 8785572c4d stash: reuse cached index entries in --patch temporary index
@@ Commit message
In a 206k file repository with `core.fsmonitor` enabled and a one-line
change in one file, time to first prompt dropped from 34.774 seconds to
- 0.659 seconds.
+ 0.659 seconds. The new perf test file demonstrates similar improvements,
+ with maen times for without- and with-fsmonitor cases dropping from 6.90
+ and 6.83 seconds to 0.55 and 0.28 seconds, respectively.
Signed-off-by: Adam Johnson <me@adamj.eu>
@@ builtin/stash.c: static int reset_tree(struct object_id *i_tree, int update, int
+ struct lock_file lock_file = LOCK_INIT;
+
+ repo_read_index_preload(the_repository, NULL, 0);
-+ if (refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL))
-+ return -1;
++ refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
+
+ hold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);
+
@@ builtin/stash.c: static int stash_patch(struct stash_info *info, const struct pa
goto done;
}
- ## t/t3904-stash-patch.sh ##
-@@ t/t3904-stash-patch.sh: test_expect_success 'none of this moved HEAD' '
- verify_saved_head
- '
-
-+test_expect_success 'stash -p with unmodified tracked files present' '
-+ git reset --hard &&
-+ echo line1 >alpha &&
-+ echo line1 >beta &&
-+ git add alpha beta &&
-+ git commit -m "add alpha and beta" &&
-+ echo line2 >>alpha &&
-+ echo y | git stash -p &&
-+ echo line1 >expect &&
-+ test_cmp expect alpha &&
-+ test_cmp expect beta &&
-+ git stash pop &&
-+ printf "line1\nline2\n" >expect &&
-+ test_cmp expect alpha &&
-+ echo line1 >expect &&
-+ test_cmp expect beta
+ ## t/perf/p3904-stash-patch.sh (new) ##
+@@
++#!/bin/sh
++
++test_description="Performance tests for git stash -p"
++
++. ./perf-lib.sh
++
++test_perf_fresh_repo
++
++test_expect_success "setup" '
++ mkdir files &&
++ test_seq 1 100000 | while read i; do
++ echo "content $i" >files/$i.txt || return 1
++ done &&
++ git add files/ &&
++ git commit -q -m "add tracked files" &&
++ echo modified >files/1.txt
+'
+
- test_expect_success 'stash -p with split hunk' '
- git reset --hard &&
- cat >test <<-\EOF &&
++test_perf "stash -p, no fsmonitor" \
++ --setup 'echo modified >files/1.txt' '
++ printf "q\n" | git stash -p >/dev/null 2>&1 || true
++'
++
++if test_have_prereq FSMONITOR_DAEMON
++then
++ test_expect_success "enable builtin fsmonitor" '
++ git config core.fsmonitor true &&
++ git fsmonitor--daemon start &&
++ git update-index --fsmonitor &&
++ git status >/dev/null 2>&1
++ '
++
++ test_perf "stash -p, builtin fsmonitor" \
++ --setup 'echo modified >files/1.txt && git status >/dev/null 2>&1' '
++ printf "q\n" | git stash -p >/dev/null 2>&1 || true
++ '
++
++ test_expect_success "stop builtin fsmonitor" '
++ git fsmonitor--daemon stop
++ '
++fi
++
++test_done
builtin/stash.c | 70 +++++++++++++++++++++++++++++++++----
t/perf/p3904-stash-patch.sh | 43 +++++++++++++++++++++++
2 files changed, 107 insertions(+), 6 deletions(-)
create mode 100755 t/perf/p3904-stash-patch.sh
diff --git a/builtin/stash.c b/builtin/stash.c
index 32dbc97b47..c4809f299a 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -372,6 +372,56 @@ static int reset_tree(struct object_id *i_tree, int update, int reset)
return 0;
}
+static int create_index_from_tree(const struct object_id *tree_id,
+ const char *index_path)
+{
+ int nr_trees = 1;
+ int ret = 0;
+ struct unpack_trees_options opts;
+ struct tree_desc t[MAX_UNPACK_TREES];
+ struct tree *tree;
+ struct index_state dst_istate = INDEX_STATE_INIT(the_repository);
+ struct lock_file lock_file = LOCK_INIT;
+
+ repo_read_index_preload(the_repository, NULL, 0);
+ refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
+
+ hold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);
+
+ memset(&opts, 0, sizeof(opts));
+
+ tree = repo_parse_tree_indirect(the_repository, tree_id);
+ if (!tree || repo_parse_tree(the_repository, tree)) {
+ ret = -1;
+ goto done;
+ }
+
+ init_tree_desc(t, &tree->object.oid, tree->buffer, tree->size);
+
+ opts.head_idx = 1;
+ opts.src_index = the_repository->index;
+ opts.dst_index = &dst_istate;
+ opts.merge = 1;
+ opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
+ opts.fn = oneway_merge;
+
+ if (unpack_trees(nr_trees, t, &opts)) {
+ ret = -1;
+ goto done;
+ }
+
+ if (write_locked_index(&dst_istate, &lock_file, COMMIT_LOCK)) {
+ ret = error(_("unable to write new index file"));
+ goto done;
+ }
+
+done:
+ release_index(&dst_istate);
+ if (ret)
+ rollback_lock_file(&lock_file);
+ return ret;
+}
+
static int diff_tree_binary(struct strbuf *out, struct object_id *w_commit)
{
struct child_process cp = CHILD_PROCESS_INIT;
@@ -1321,18 +1371,26 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct interactive_options *interactive_opts)
{
int ret = 0;
- struct child_process cp_read_tree = CHILD_PROCESS_INIT;
struct child_process cp_diff_tree = CHILD_PROCESS_INIT;
+ struct commit *head_commit;
+ const struct object_id *head_tree;
struct index_state istate = INDEX_STATE_INIT(the_repository);
char *old_index_env = NULL, *old_repo_index_file;
remove_path(stash_index_path.buf);
- cp_read_tree.git_cmd = 1;
- strvec_pushl(&cp_read_tree.args, "read-tree", "HEAD", NULL);
- strvec_pushf(&cp_read_tree.env, "GIT_INDEX_FILE=%s",
- stash_index_path.buf);
- if (run_command(&cp_read_tree)) {
+ head_commit = lookup_commit(the_repository, &info->b_commit);
+ if (!head_commit || repo_parse_commit(the_repository, head_commit)) {
+ ret = -1;
+ goto done;
+ }
+ head_tree = get_commit_tree_oid(head_commit);
+ if (!head_tree) {
+ ret = -1;
+ goto done;
+ }
+
+ if (create_index_from_tree(head_tree, stash_index_path.buf)) {
ret = -1;
goto done;
}
diff --git a/t/perf/p3904-stash-patch.sh b/t/perf/p3904-stash-patch.sh
new file mode 100755
index 0000000000..4cfce638be
--- /dev/null
+++ b/t/perf/p3904-stash-patch.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description="Performance tests for git stash -p"
+
+. ./perf-lib.sh
+
+test_perf_fresh_repo
+
+test_expect_success "setup" '
+ mkdir files &&
+ test_seq 1 100000 | while read i; do
+ echo "content $i" >files/$i.txt || return 1
+ done &&
+ git add files/ &&
+ git commit -q -m "add tracked files" &&
+ echo modified >files/1.txt
+'
+
+test_perf "stash -p, no fsmonitor" \
+ --setup 'echo modified >files/1.txt' '
+ printf "q\n" | git stash -p >/dev/null 2>&1 || true
+'
+
+if test_have_prereq FSMONITOR_DAEMON
+then
+ test_expect_success "enable builtin fsmonitor" '
+ git config core.fsmonitor true &&
+ git fsmonitor--daemon start &&
+ git update-index --fsmonitor &&
+ git status >/dev/null 2>&1
+ '
+
+ test_perf "stash -p, builtin fsmonitor" \
+ --setup 'echo modified >files/1.txt && git status >/dev/null 2>&1' '
+ printf "q\n" | git stash -p >/dev/null 2>&1 || true
+ '
+
+ test_expect_success "stop builtin fsmonitor" '
+ git fsmonitor--daemon stop
+ '
+fi
+
+test_done
base-commit: 7bcaabddcf68bd0702697da5904c3b68c52f94cf
--
gitgitgadget
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox