Git development
 help / color / mirror / Atom feed
* Re: [PATCH v2] stash: reuse cached index entries in --patch temporary index
From: Junio C Hamano @ 2026-06-01 21:33 UTC (permalink / raw)
  To: Adam Johnson via GitGitGadget
  Cc: git, Thomas Gummerer, Elijah Newren, Phillip Wood, Victoria Dye,
	Adam Johnson
In-Reply-To: <pull.2306.v2.git.git.1779491545531.gitgitgadget@gmail.com>

"Adam Johnson via GitGitGadget" <gitgitgadget@gmail.com> writes:

> 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

The diff relative to the previous round looked good.  I am not a
"stash -p" user myself, but I suspect that there are people who
heavily use it, so I'd feel safer if an extra set of eye looks at
the patch and gives an Ack, but other than that I have no comments
on the patch.  Looking good.

Thanks.


>  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

^ permalink raw reply

* Re: [PATCH 00/18] odb: make loose object source a proper `struct odb_source`
From: Junio C Hamano @ 2026-06-01 21:33 UTC (permalink / raw)
  To: Patrick Steinhardt; +Cc: git
In-Reply-To: <20260521-b4-pks-odb-source-loose-v1-0-6553b399be2d@pks.im>

Patrick Steinhardt <ps@pks.im> writes:

> Hi,
>
> this patch series converts the loose object source into a proper `struct
> odb_source` so that it can be used via our generic interfaces.
>
> The patch series is relatively straight-forward, as the source basically
> already exists as such and the interfaces already match. So for most of
> the part we are just moving around some code and converting functions
> that were previously called directly into callbacks.
>
> I guess the only part that needs some attention is that there is some
> confusion at first with the `struct odb_source_loose::source` parent
> pointer that initially points at the owning `struct odb_source_files`.
> This relationship doesn't make much sense, as a loose source can totally
> exist standalone without the files source.

No significant comments came in the past week or so on these
patches.  Should we declare victory, and mark it for 'next'?  I can
locally amend a typo in [3/18] (<xmqqh5o0zrsr.fsf@gitster.g>).

^ permalink raw reply

* Re: Missing Git Features for Modern Multi-Repository, Dependency-Driven Development
From: Skybuck Flying @ 2026-06-01 21:32 UTC (permalink / raw)
  To: Git
In-Reply-To: <AM0PR02MB4450F6AF2F662C51F3145B48B3152@AM0PR02MB4450.eurprd02.prod.outlook.com>

Point 7 needs further explaining and this document will do so:

7. **Stable repository identity**

---

# ⭐ **SPECIFICATION: Stable Repository Identity for Git**

Modern software projects increasingly rely on large dependency graphs, multi‑repository structures, reproducible builds, and long‑term provenance. Git provides excellent version control but lacks native mechanisms for these workflows.  
This specification outlines optional, backward‑compatible metadata extensions that would allow Git to better support modern development practices.

This document contains four sections:

111. WHAT GIT SHOULD HAVE BEEN  
222. RFC / PROPOSAL  
333. IMPLEMENTATION IDEAS / DETAILS  
444. EXAMPLES  

---

# ⭐ **111. WHAT GIT SHOULD HAVE BEEN — Stable Repository Identity**

Git is brilliant at what it was designed for:

- content‑addressable storage  
- distributed history  
- immutable snapshots  

But Git has one deep architectural flaw:

> **A repository’s identity = its URL.**

This has caused 15+ years of breakage across ecosystems:

- Go import paths break when repos move  
- mirrors confuse tooling  
- forks lose provenance  
- dependency manifests rot  
- security advisories become invalid  
- organizational migrations break everything  
- renames break imports and builds  

Git treats the *transport location* as the *identity*.  
This is backwards.

---

## ⭐ **Basic Idea (3–5 lines)**  
Git breaks when a repository’s URL changes because the URL *is* the identity.  
The fix is to give every repo a permanent UUID stored in `.git/identity`.  
Tools then import using a stable name like `sky/yaml`, which Git maps to the UUID.  
If the repo moves, renames, or changes hosting, only the mapping updates — **the code stays the same**.

---

Git should have had:

### ✔ A stable, permanent identity  
### ✔ Independent of URL  
### ✔ Independent of hosting provider  
### ✔ Independent of username  
### ✔ Independent of organization  
### ✔ Independent of mirrors and forks  

This identity should have been:

- generated once  
- stored inside the repo  
- immutable  
- portable  
- cryptographically strong  

Something like:

```
.git/identity
uuid = "d8f1-9c2e-44b1-8f3a-abc123"
```

This would have allowed:

- renaming  
- moving  
- mirroring  
- forking  
- reorganizing  
- migrating hosts  

**without breaking anything.**

Git should have been built on **stable identity**, not URLs.

---

# ⭐ **222. RFC: Stable Repository Identity for Git**

## **1. Introduction**

Git repositories today are identified by their URLs.  
This creates long‑term fragility in:

- dependency management  
- import paths  
- provenance tracking  
- fork lineage  
- security metadata  
- multi‑repo systems  
- organizational migrations  

This RFC proposes a minimal, optional, backward‑compatible mechanism for assigning **stable identities** to Git repositories.

---

## **2. Problem Statement**

Git currently lacks:

- a persistent repository identity  
- a way to track renames  
- a way to track hosting moves  
- a way to track mirrors  
- a way to track forks  
- a way to track upstream provenance  
- a way to reference repositories independent of URLs  

As a result:

- Go import paths break  
- dependency manifests rot  
- security advisories become invalid  
- forks lose their origin  
- mirrors cannot be recognized  
- tools cannot detect duplicates  
- organizations cannot reorganize safely  

Git’s URL‑based identity model is insufficient for modern software engineering.

---

## **3. Proposed Feature: `.git/identity`**

Introduce a file:

```
.git/identity
```

Containing:

```
uuid = "<128-bit UUID>"
```

### **Properties**

- generated once at `git init`  
- immutable  
- portable  
- stored inside the repository  
- independent of hosting  
- independent of remotes  
- independent of URLs  
- independent of usernames  

### **Benefits**

- stable identity across renames  
- stable identity across mirrors  
- stable identity across forks  
- stable identity across hosting providers  
- stable identity across organizational changes  

---

## **4. Use Cases**

### **4.1. Import Path Stability**

Tools and languages can reference:

```
uuid:d8f1-9c2e-44b1-8f3a-abc123
```

instead of:

```
github.com/user/project
```

Renames no longer break imports.

---

### **4.2. Dependency Manifest Stability**

Manifests can store:

```
yaml = "uuid:d8f1-9c2e-44b1-8f3a-abc123"
```

instead of URLs.

This prevents dependency rot.

---

### **4.3. Provenance Tracking**

Forks can store:

```
origin_uuid = "d8f1-9c2e-44b1-8f3a-abc123"
```

Git can detect:

- upstream  
- divergence  
- last sync  
- fork lineage  

---

### **4.4. Security Metadata Stability**

Security advisories can reference UUIDs instead of URLs.

This prevents advisories from breaking when repos move.

---

## **5. Backward Compatibility**

- optional  
- ignored by older Git versions  
- does not affect existing workflows  
- does not change commit formats  
- does not change remotes  
- does not change URLs  
- does not break anything  

---

## **6. Conclusion**

A stable repository identity is a minimal, optional enhancement that solves long‑standing structural issues in Git’s architecture. It enables robust dependency management, provenance tracking, security metadata, and multi‑repo tooling.

---

# ⭐ **333. IMPLEMENTATION IDEAS / DETAILS**

This section describes how hosting providers (GitHub, GitLab, Gitea, Bitbucket, self‑hosted servers) could implement and expose stable repository identities using the proposed `.git/identity` file.

---

## **3.1. Repository UUID Storage**

When a repository is pushed, the hosting provider reads:

```
.git/identity
uuid = "<128-bit UUID>"
```

The platform stores this UUID in its internal metadata database.

If the file does not exist, the platform may:

- generate a UUID on first push, or  
- leave the field empty (backward compatibility)  

---

## **3.2. URL Structure and Access**

### **Human‑friendly URLs remain unchanged**

```
https://github.com/SkybuckFlying/yaml
https://gitlab.com/sky/yaml
```

### **Optional UUID‑based URLs**

```
https://github.com/uuid/d8f1-9c2e-44b1-8f3a-abc123
https://gitlab.com/uuid/d8f1-9c2e-44b1-8f3a-abc123
```

These URLs:

- never change  
- always resolve to the current location  
- survive renames, moves, and hosting migrations  

---

## **3.3. Redirect Behavior**

If a repository is renamed:

```
/yaml → /yaml2
```

UUID URL still works.

If moved to another user or organization:

```
/SkybuckFlying/yaml → /sky-org/yaml
```

UUID URL still works.

If moved to another hosting provider:

```
github → gitlab
```

UUID URL still works.

---

## **3.4. Fork and Mirror Detection**

With UUIDs, platforms can detect:

- mirrors (same UUID, same commit graph)  
- forks (different UUID, but `.gitorigin` references parent UUID)  
- duplicates (same UUID, different URLs)  

This enables:

- accurate fork lineage  
- upstream tracking  
- divergence analysis  
- provenance reconstruction  

---

## **3.5. Cloning by UUID**

Git could support:

```
git clone uuid:d8f1-9c2e-44b1-8f3a-abc123
```

Git resolves the UUID by:

- checking `.gitimports`  
- checking local registry  
- querying hosting providers  
- falling back to known mirrors  

---

## **3.6. Dependency Resolution Using UUIDs**

Manifests can reference UUIDs:

```
yaml = "uuid:d8f1-9c2e-44b1-8f3a-abc123"
```

Tools resolve the UUID to a URL using:

- `.gitimports`  
- hosting provider lookup  
- local cache  

This prevents dependency rot.

---

## **3.7. Security Metadata Integration**

Security advisories can reference UUIDs:

```
uuid = "d8f1-9c2e-44b1-8f3a-abc123"
cve = ["CVE-2022-28948"]
```

Platforms can warn users when:

- cloning vulnerable repos  
- checking out vulnerable commits  

---

## **3.8. Backward Compatibility**

- Repositories without `.git/identity` continue to work  
- Tools ignoring UUIDs continue to work  
- URLs remain unchanged  
- No protocol changes  
- No breaking changes  

---

# ⭐ **444. EXAMPLES — Why Stable Identity Matters**

## **Example 1: Go Import Path Breakage**

Today:

```
import "github.com/user1/yaml"
```

User renames account → imports break.

With UUIDs:

```
import "sky/yaml"
```

Mapped via:

```
sky/yaml = "uuid:d8f1-9c2e-44b1-8f3a-abc123"
```

Repo moves → nothing breaks.

---

## **Example 2: Fork Provenance Loss**

Today Git does not know:

- what repo a fork came from  
- when it was last synced  
- how far it diverged  

With UUIDs:

```
.gitorigin
origin_uuid = "d8f1-9c2e-44b1-8f3a-abc123"
last_synced = "a3f1234"
```

Git can track upstream properly.

---

## **Example 3: Security Advisory Stability**

Today advisories reference URLs:

```
CVE-2022-28948 affects github.com/user/yaml
```

Repo moves → advisory becomes ambiguous.

With UUIDs:

```
uuid = "d8f1-9c2e-44b1-8f3a-abc123"
cve = ["CVE-2022-28948"]
```

Advisory remains valid forever.

---

## **Example 4: Organizational Migration**

Company moves from GitHub → GitLab.

Today:

- all URLs change  
- manifests break  
- imports break  
- tooling breaks  

With UUIDs:

- identity stays the same  
- manifests stay valid  
- imports stay valid  
- tooling stays valid  

Only the URL mapping changes.

---

## **Example 5: Mirror Detection**

Today Git cannot tell:

- if two URLs point to the same repo  
- if a repo is a mirror  
- if a repo is a duplicate  

With UUIDs:

```
uuid = "d8f1-9c2e-44b1-8f3a-abc123"
```

Git instantly knows:

> “These two repos are the same project.”

---

Bye for now,  
  Skybuck Flying / Harald Houppermans ! ;) =D XD

^ permalink raw reply

* Re: [GSoC][PATCH 4/4] repo: add path.commondir with absolute and relative suffix formatting
From: Lucas Seiki Oshiro @ 2026-06-01 16:34 UTC (permalink / raw)
  To: K Jayatheerth
  Cc: git, jltobler, gitster, phillip.wood, sandals, kumarayushjha123,
	a3205153416
In-Reply-To: <20260601151950.30686-5-jayatheerthkulkarni2005@gmail.com>


> diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
> index 7c7dfbb052..dd2706e1f7 100755
> --- a/t/t1900-repo-info.sh
> +++ b/t/t1900-repo-info.sh
> @@ -184,6 +184,7 @@ test_expect_success 'setup test repository layout for path fields' '
> mkdir -p test-repo/sub
> '
> 
> +test_repo_info_path 'commondir' '../.git'
> test_repo_info_path 'gitdir' '../.git'

I was thinking here, maybe you need to take a look at
git-rev-parse's tests and check what are the corner cases.

For example, `git rev-parse --git-common-dir` documentation
says:

    --git-common-dir:
        Show $GIT_COMMON_DIR if defined, else $GIT_DIR

This way, you should take a look on how git-rev-parse tests
test those two branches (GIT_COMMON_DIR and GIT_DIR).


^ permalink raw reply

* Re: [PATCH 2/2] builtin/init-db: deprecate alias for git-init(1)
From: Kristoffer Haugsbakk @ 2026-06-01 21:23 UTC (permalink / raw)
  To: Patrick Steinhardt, Phillip Wood; +Cc: git
In-Reply-To: <ah2VL-ftCQelNoOc@pks.im>

By the way I tried to find user mentions of git-init-db(1) on the
mailing list. (First git then I tried *all* but the results did not seem
dissimilar at all.) All I found was from last year[1] but the command
was used as a bug reproducer, which hints at some finger memory.

🔗 1: https://lore.kernel.org/git/d8c1df4e-a4d7-4c4c-be44-b13de3d9ffea@markus-raab.org/

On Mon, Jun 1, 2026, at 16:20, Patrick Steinhardt wrote:
> On Mon, Jun 01, 2026 at 02:48:05PM +0100, Phillip Wood wrote:
>>[snip
>>
>> Deprecating this command seems very sensible to me. As well as marking it
>> deprecated, do we want to print a warning when it is run? I imagine anyone
>> who has this command in their muscle memory is unlikely to be reading the
>> man page on a regular basis so wont see the warning there.
>
> I was wondering whether we want to call `you_still_use_that()` here.

As-is that will arguably promote the *breaking change* to right now
since it’s a `die(...)` function. That could be changed to be warn/die
modular of course.

But a simple warning message can just tell them to use git-init(1).

> I found it to be a bit heavy-handed as it's so trivial to replace with
> git-init(1), but on the other hand it's a trivial thing to do.

I imagine that most potential git-init-db(1) uses will be buried in some
scripts that haven’t been touched in years. Then the Git init might
fail, you get errors about git-commit(1) or something not being a thing
you can run without a repository, and it ends up being a headscratcher
since the original failure gets lost.

All to say I think a simple warning would be nice. ;)

^ permalink raw reply

* [PATCH v2] sub-process: use gentle handshake to avoid die() on startup failure
From: Michael Montalbo via GitGitGadget @ 2026-06-01 21:20 UTC (permalink / raw)
  To: git; +Cc: Michael Montalbo, Michael Montalbo
In-Reply-To: <pull.2133.git.1780287309846.gitgitgadget@gmail.com>

From: Michael Montalbo <mmontalbo@gmail.com>

When the configured subprocess command contains shell metacharacters
(such as a space), prepare_shell_cmd() wraps it in "sh -c <cmd>".
The shell itself always starts successfully, so start_command()
returns zero even if the tool inside does not exist.  The subsequent
handshake then reads from a dead pipe and calls die() via the
non-gentle packet_read_line(), killing the parent process instead of
letting it handle the error.

Before this change, a missing filter process at a path containing
spaces produces a confusing error:

    $ git -c filter.myfilter.process="/path with space/tool" \
          -c filter.myfilter.required=true add file.txt
    /path with space/tool: line 1: /path: No such file or directory
    fatal: the remote end hung up unexpectedly

After this change, the proper error is reported:

    $ git ... add file.txt
    /path with space/tool: line 1: /path: No such file or directory
    error: could not read greeting from subprocess '/path with space/tool'
    error: initialization for subprocess '/path with space/tool' failed
    fatal: file.txt: clean filter 'myfilter' failed

Switch the subprocess handshake from the dying packet_read_line()
to packet_read_line_gently() so that a process that exits during
startup produces an error return instead of killing the caller.

This affects any subprocess consumer whose command path contains
spaces.  On Windows this routinely happens because programs live
under "C:/Program Files/...", and MSYS2 path conversion can rewrite
absolute paths to include that prefix.  On POSIX it triggers
whenever the configured path naturally contains a space or other
metacharacter.  convert.c (filter.<driver>.process, used by git-lfs
and custom clean/smudge filters) is the primary affected consumer.

Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
---
    sub-process: use gentle handshake to avoid die() on startup failure

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2133%2Fmmontalbo%2Fmm%2Fsubprocess-handshake-fix-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2133/mmontalbo/mm/subprocess-handshake-fix-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2133

Range-diff vs v1:

 1:  c11b01c156 ! 1:  cb7bd777dc sub-process: use gentle handshake to avoid die() on startup failure
     @@ Commit message
          Before this change, a missing filter process at a path containing
          spaces produces a confusing error:
      
     -        $ git -c filter.lfs.process="/path with space/tool" \
     -              -c filter.lfs.required=true add file.txt
     +        $ git -c filter.myfilter.process="/path with space/tool" \
     +              -c filter.myfilter.required=true add file.txt
     +        /path with space/tool: line 1: /path: No such file or directory
              fatal: the remote end hung up unexpectedly
      
          After this change, the proper error is reported:
      
              $ git ... add file.txt
     +        /path with space/tool: line 1: /path: No such file or directory
     +        error: could not read greeting from subprocess '/path with space/tool'
              error: initialization for subprocess '/path with space/tool' failed
     -        fatal: file.txt: clean filter 'lfs' failed
     +        fatal: file.txt: clean filter 'myfilter' failed
      
          Switch the subprocess handshake from the dying packet_read_line()
          to packet_read_line_gently() so that a process that exits during
     @@ sub-process.c: static int handshake_version(struct child_process *process,
       		return error("Could not write flush packet");
       
      -	if (!(line = packet_read_line(process->out, NULL)) ||
     -+	if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
     - 	    !skip_prefix(line, welcome_prefix, &p) ||
     +-	    !skip_prefix(line, welcome_prefix, &p) ||
     ++	if (packet_read_line_gently(process->out, NULL, &line) < 0)
     ++		return error("could not read greeting from subprocess '%s'",
     ++			     process->args.v[0]);
     ++	if (!line || !skip_prefix(line, welcome_prefix, &p) ||
       	    strcmp(p, "-server"))
       		return error("Unexpected line '%s', expected %s-server",
       			     line ? line : "<flush packet>", welcome_prefix);
      -	if (!(line = packet_read_line(process->out, NULL)) ||
     -+	if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
     - 	    !skip_prefix(line, "version=", &p) ||
     +-	    !skip_prefix(line, "version=", &p) ||
     ++	if (packet_read_line_gently(process->out, NULL, &line) < 0)
     ++		return error("could not read version from subprocess '%s'",
     ++			     process->args.v[0]);
     ++	if (!line || !skip_prefix(line, "version=", &p) ||
       	    strtol_i(p, 10, chosen_version))
       		return error("Unexpected line '%s', expected version",
       			     line ? line : "<flush packet>");
      -	if ((line = packet_read_line(process->out, NULL)))
     --		return error("Unexpected line '%s', expected flush", line);
     -+	if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
     -+		return error("Unexpected line '%s', expected flush",
     -+			     line ? line : "<read error>");
     ++	if (packet_read_line_gently(process->out, NULL, &line) < 0)
     ++		return error("could not read version flush from subprocess '%s'",
     ++			     process->args.v[0]);
     ++	if (line)
     + 		return error("Unexpected line '%s', expected flush", line);
       
       	/* Check to make sure that the version received is supported */
     - 	for (i = 0; versions[i]; i++) {
      @@ sub-process.c: static int handshake_capabilities(struct child_process *process,
       	if (packet_flush_gently(process->in))
       		return error("Could not write flush packet");
       
      -	while ((line = packet_read_line(process->out, NULL))) {
     -+	while (packet_read_line_gently(process->out, NULL, &line) > 0) {
     ++	for (;;) {
       		const char *p;
     ++		int len = packet_read_line_gently(process->out, NULL, &line);
     ++
     ++		if (len < 0)
     ++			return error("could not read capabilities from subprocess '%s'",
     ++				     process->args.v[0]);
     ++		if (!line)
     ++			break;
       		if (!skip_prefix(line, "capability=", &p))
       			continue;
     + 
      
       ## t/t0021-conversion.sh ##
      @@ t/t0021-conversion.sh: test_expect_success 'invalid process filter must fail (and not hang!)' '


 sub-process.c         | 26 ++++++++++++++++++++------
 t/t0021-conversion.sh | 17 +++++++++++++++++
 2 files changed, 37 insertions(+), 6 deletions(-)

diff --git a/sub-process.c b/sub-process.c
index 83bf0a0e82..2d5c965169 100644
--- a/sub-process.c
+++ b/sub-process.c
@@ -132,17 +132,24 @@ static int handshake_version(struct child_process *process,
 	if (packet_flush_gently(process->in))
 		return error("Could not write flush packet");
 
-	if (!(line = packet_read_line(process->out, NULL)) ||
-	    !skip_prefix(line, welcome_prefix, &p) ||
+	if (packet_read_line_gently(process->out, NULL, &line) < 0)
+		return error("could not read greeting from subprocess '%s'",
+			     process->args.v[0]);
+	if (!line || !skip_prefix(line, welcome_prefix, &p) ||
 	    strcmp(p, "-server"))
 		return error("Unexpected line '%s', expected %s-server",
 			     line ? line : "<flush packet>", welcome_prefix);
-	if (!(line = packet_read_line(process->out, NULL)) ||
-	    !skip_prefix(line, "version=", &p) ||
+	if (packet_read_line_gently(process->out, NULL, &line) < 0)
+		return error("could not read version from subprocess '%s'",
+			     process->args.v[0]);
+	if (!line || !skip_prefix(line, "version=", &p) ||
 	    strtol_i(p, 10, chosen_version))
 		return error("Unexpected line '%s', expected version",
 			     line ? line : "<flush packet>");
-	if ((line = packet_read_line(process->out, NULL)))
+	if (packet_read_line_gently(process->out, NULL, &line) < 0)
+		return error("could not read version flush from subprocess '%s'",
+			     process->args.v[0]);
+	if (line)
 		return error("Unexpected line '%s', expected flush", line);
 
 	/* Check to make sure that the version received is supported */
@@ -171,8 +178,15 @@ static int handshake_capabilities(struct child_process *process,
 	if (packet_flush_gently(process->in))
 		return error("Could not write flush packet");
 
-	while ((line = packet_read_line(process->out, NULL))) {
+	for (;;) {
 		const char *p;
+		int len = packet_read_line_gently(process->out, NULL, &line);
+
+		if (len < 0)
+			return error("could not read capabilities from subprocess '%s'",
+				     process->args.v[0]);
+		if (!line)
+			break;
 		if (!skip_prefix(line, "capability=", &p))
 			continue;
 
diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh
index f0d50d769e..033b00a364 100755
--- a/t/t0021-conversion.sh
+++ b/t/t0021-conversion.sh
@@ -857,6 +857,23 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '
 	)
 '
 
+test_expect_success 'missing process filter with space in path does not die' '
+	test_config_global filter.protocol.process "/non existent/tool" &&
+	test_config_global filter.protocol.required true &&
+	rm -rf repo &&
+	mkdir repo &&
+	(
+		cd repo &&
+		git init &&
+
+		echo "*.r filter=protocol" >.gitattributes &&
+
+		cp "$TEST_ROOT/test.o" test.r &&
+		test_must_fail git add . 2>git-stderr.log &&
+		test_grep "clean filter.*protocol.*failed" git-stderr.log
+	)
+'
+
 test_expect_success 'delayed checkout in process filter' '
 	test_config_global filter.a.process "test-tool rot13-filter --log=a.log clean smudge delay" &&
 	test_config_global filter.a.required true &&

base-commit: 29bd7ed5127255713c1ac2f43b7c6f257d7b4594
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH v2] doc: fix typos via codespell
From: Kristoffer Haugsbakk @ 2026-06-01 20:59 UTC (permalink / raw)
  To: Junio C Hamano, Andrew Kreimer; +Cc: git
In-Reply-To: <xmqqo6hv9i1w.fsf@gitster.g>

On Mon, Jun 1, 2026, at 03:16, Junio C Hamano wrote:
>[snip]
>
> However, there are things that BREAK tests.
>
>> diff --git a/t/t1700-split-index.sh b/t/t1700-split-index.sh
>> index ac4a5b2734..869fb4a14e 100755
>> --- a/t/t1700-split-index.sh
>> +++ b/t/t1700-split-index.sh
>> @@ -502,7 +502,7 @@ test_expect_success 'do not refresh null base index' '
>>  		git checkout main &&
>>  		git update-index --split-index &&
>>  		test_commit more &&
>> -		# must not write a new shareindex, or we wont catch the problem
>> +		# must not write a new shareindex, or we won't catch the problem
>>  		git -c splitIndex.maxPercentChange=100 merge --no-edit side-branch 2>err &&
>>  		# i.e. do not expect warnings like
>>  		# could not freshen shared index .../shareindex.00000...
>
> The edit above is made to a STRING that is enclosed inside a pair of
> single quote.  If we want to use "won't", we would need to write "We
> won'\''t", but while it may be syntactically correct as a part of
> shell script, it is a pointless change, as the target audience wants
> to see this line as if it is just a plain text.

Sorry about not testing this on v1. “Surely this does not affect the
code...” strikes again.

>
> "We will not" would be acceptable,
>
>> diff --git a/t/t3909-stash-pathspec-file.sh b/t/t3909-stash-pathspec-file.sh
>>[snip]

^ permalink raw reply

* Missing Git Features for Modern Multi-Repository, Dependency-Driven Development
From: Skybuck Flying @ 2026-06-01 20:57 UTC (permalink / raw)
  To: Git

Modern software projects increasingly rely on large dependency graphs, multi-repository structures, 
reproducible builds, and long-term provenance. Git provides excellent version control but lacks native mechanisms 
for these workflows. 
This RFC outlines optional, backward-compatible metadata extensions that would allow Git to better support modern development practices.


This document contains three sections:

111. WHAT GIT SHOULD HAVE BEEN
222. RFC/PROPOSAL SECTION
333. EXAMPLE SECTION (Examples of why it would be usefull)

***
111. WHAT GIT SHOULD HAVE BEEN
***

---

# ⭐ **REPORT: Missing Git Features Required for a Complete Modern System**

Git is brilliant at what it *was designed for*:

- content-addressable storage  
- distributed history  
- immutable snapshots  

But Git is **NOT** a complete software-engineering system.

Below is the list of features Git *should* have had to avoid the chaos of:

- meaningless import paths  
- dependency hell  
- submodule drift  
- missing provenance  
- missing security metadata  
- missing version semantics  
- missing reproducibility  
- missing dependency manifests  

These are the features Git is missing.

---

# ⭐ 1. **A Built-In Dependency Manifest System**
Git should have had:

### ✔ A first-class manifest file  
Something like:

```
.gitdeps
```

Containing:

- dependency name  
- dependency URL  
- dependency version  
- dependency commit  
- dependency purpose  
- dependency license  
- dependency security metadata  

### ✔ A built-in lockfile  
Something like:

```
.gitdeps.lock
```

Freezing:

- exact commit hashes  
- exact versions  
- exact URLs  

### ✔ Automatic dependency resolution  
Like Cargo, Go, npm, Maven, Gradle.

### ✔ Automatic dependency graph visualization  
Not “git submodule status”.

### ✔ Automatic reproducible builds  
Not “hope the submodules are correct”.

---

# ⭐ 2. **Semantic Versioning Support**
Git should have supported:

### ✔ Version numbers  
Not just tags.

### ✔ Version ranges  
Not just commit hashes.

### ✔ Version constraints  
Not just “latest tag”.

### ✔ Version compatibility rules  
Not just “merge and pray”.

### ✔ Version negotiation  
Not just “clone and hope”.

---

# ⭐ 3. **Built-In Provenance Tracking**
Git should have stored:

### ✔ Original upstream URL  
### ✔ Fork lineage  
### ✔ Migration history  
### ✔ Renames  
### ✔ Moves  
### ✔ Repository identity  

This should have been **inside the repo metadata**, not:

- lost  
- local only  
- dependent on remotes  
- dependent on GitHub  
- dependent on user discipline  

You should NEVER lose:

- where a repo came from  
- who forked it  
- why it exists  
- what it was based on  

Git does not store this.  
It should.

---

# ⭐ 4. **Built-In Security Metadata**
Git should have had:

### ✔ CVE metadata per commit  
### ✔ Security advisories per tag  
### ✔ Vulnerability scanning  
### ✔ Security provenance  
### ✔ Signed dependency manifests  
### ✔ Automatic alerts when upstream is compromised  

Instead, we have:

- nothing  
- external tools  
- Go’s vuln system (only for Go modules)  
- GitHub advisories (platform-specific)  

Git should have had this **natively**.

---

# ⭐ 5. **Built-In Fork Synchronization**
Git should have supported:

### ✔ Automatic upstream tracking  
### ✔ Automatic upstream diffing  
### ✔ Automatic upstream merge suggestions  
### ✔ Automatic conflict detection  
### ✔ Automatic patch propagation  

Instead, we have:

- manual remotes  
- manual fetch  
- manual merge  
- manual conflict resolution  

Git should have had a **first-class fork model**.

---

# ⭐ 6. **Built-In Repository Renaming Without Breaking Imports**
Git should have supported:

### ✔ Stable repository identity  
### ✔ Stable import identity  
### ✔ Stable module identity  
### ✔ Renames without breakage  
### ✔ Moves without breakage  
### ✔ Aliases  

Instead, Go import paths break if:

- the repo moves  
- the user changes their username  
- the repo is renamed  
- the platform changes  
- the domain changes  

Git should have had **stable identity**, not “URL = identity”.

---

# ⭐ 7. **Built-In Multi-Repo Project Support**
Git should have supported:

### ✔ Multi-repo projects  
### ✔ Multi-repo manifests  
### ✔ Multi-repo versioning  
### ✔ Multi-repo snapshots  
### ✔ Multi-repo reproducibility  

Instead, we have:

- submodules (broken)  
- subtrees (hacky)  
- monorepos (workaround)  
- external tools (Bazel, Buck, Pants)  

Git should have had **native multi-repo support**.

---

# ⭐ 8. **Built-In Metadata Files for Remote Code**
Git should have supported:

### ✔ `.gitorigin`  
Stores original upstream URL.

### ✔ `.gitpurpose`  
Stores why the repo exists.

### ✔ `.gitfork`  
Stores fork lineage.

### ✔ `.gitsecurity`  
Stores security metadata.

### ✔ `.gitdeps`  
Stores dependency graph.

Instead, we have:

- nothing  
- manual text files  
- tribal knowledge  

Git should have had **first-class metadata**.

---

# ⭐ 9. **Built-In Import Path Abstraction**
Git should have supported:

### ✔ Clean import names  
### ✔ Semantic import names  
### ✔ Import aliases  
### ✔ Import remapping  
### ✔ Import rewriting  

Without breaking:

- security scanning  
- module identity  
- provenance  

Instead, languages like Go embed garbage URLs into source code.

Git should have provided a **clean abstraction layer**.

---

# ⭐ 10. **Built-In Reproducible Snapshots**
Git should have supported:

### ✔ Project-level snapshots  
### ✔ Dependency snapshots  
### ✔ Multi-repo snapshots  
### ✔ Build snapshots  
### ✔ Environment snapshots  

Instead, reproducibility is:

- manual  
- fragile  
- external  
- inconsistent  

Git should have had **snapshot manifests**.

---

# ⭐ FINAL SUMMARY — WHAT GIT SHOULD HAVE BEEN

Git should have included:

1. **Dependency manifest system**  
2. **Lockfile system**  
3. **Semantic versioning**  
4. **Provenance tracking**  
5. **Security metadata**  
6. **Fork synchronization**  
7. **Stable repository identity**  
8. **Multi-repo project support**  
9. **Import path abstraction**  
10. **Reproducible snapshots**

If Git had these features, you would NOT be suffering:

- meaningless import paths  
- dependency chaos  
- submodule hell  
- lost provenance  
- broken security scanning  
- non-future-proof naming  
- manual patch tracking  
- manual manifest creation  
- manual Delphi porting  

Git is brilliant — but incomplete.


***
222 RFC/PROPOSAL SECTION:
***

---

# **RFC: Proposal for Enhancing Git with First-Class Dependency, Provenance, and Security Metadata**

---

## **1. Introduction**

Git is exceptionally strong as a distributed version-control system, but modern software development increasingly relies on:

- multi-repository architectures  
- dependency graphs  
- reproducible builds  
- long-term provenance  
- fork synchronization  
- security metadata  
- stable module identities  

These requirements are now fundamental to large-scale software engineering, yet Git provides no native mechanisms for them. As a result, ecosystems (Go, Rust, npm, Cargo, Maven, etc.) have built their own parallel systems on top of Git to compensate for missing features.

This RFC proposes a set of enhancements that would allow Git to natively support these modern workflows.

---

## **2. Problem Statement**

Git repositories today lack:

1. **Dependency manifests**  
2. **Dependency lockfiles**  
3. **Provenance metadata**  
4. **Fork lineage tracking**  
5. **Stable repository identity independent of URL**  
6. **Security advisory integration**  
7. **Multi-repository project support**  
8. **Import path abstraction**  
9. **Reproducible multi-repo snapshots**

These gaps force developers to rely on:

- ad-hoc conventions  
- external package managers  
- fragile submodules  
- undocumented local remotes  
- manual patch tracking  
- URL-encoded identities  
- platform-specific metadata (GitHub/GitLab)  

This creates long-term maintainability issues, especially when:

- repositories are renamed  
- maintainers disappear  
- URLs change  
- forks diverge  
- security advisories are issued  
- dependency graphs grow large  

Git’s current feature set is insufficient for these realities.

---

## **3. Proposed Features**

### **3.1. First-Class Dependency Manifest**

Introduce a repository-level file:

```
.gitdeps
```

Containing:

- dependency name  
- dependency URL  
- dependency version or commit  
- purpose/description  
- license metadata  

This would be analogous to:

- go.mod  
- Cargo.toml  
- package.json  
- Maven pom.xml  

but standardized at the Git level.

---

### **3.2. Dependency Lockfile**

Introduce:

```
.gitdeps.lock
```

Containing:

- exact commit hashes  
- integrity hashes  
- reproducible snapshot metadata  

This enables deterministic builds across machines and time.

---

### **3.3. Provenance Metadata**

Introduce:

```
.gitorigin
```

Containing:

- original upstream URL  
- fork lineage  
- migration history  
- repository identity (stable UUID)  

This prevents loss of provenance when:

- remotes are removed  
- repositories are renamed  
- repositories move between hosts  

---

### **3.4. Stable Repository Identity**

Introduce a **repository UUID** stored in `.git/identity`.

This would allow:

- renaming  
- moving  
- mirroring  
- hosting changes  

without breaking:

- import paths  
- dependency manifests  
- security metadata  
- tooling  

This solves the long-standing problem of “URL = identity”.

---

### **3.5. Security Metadata Integration**

Introduce:

```
.gitsecurity
```

Containing:

- CVE metadata  
- advisory links  
- affected versions  
- patched versions  
- severity  

This allows Git to:

- warn on checkout  
- warn on merge  
- warn on dependency resolution  

without relying on external platforms.

---

### **3.6. Fork Synchronization Metadata**

Introduce:

```
.gitfork
```

Containing:

- upstream URL  
- last synced commit  
- divergence metadata  
- pending upstream changes  

This enables:

- automated fork synchronization  
- upstream diffing  
- patch propagation  

---

### **3.7. Multi-Repository Project Support**

Introduce:

```
.gitproject
```

Containing:

- list of repositories  
- versions/commits  
- dependency graph  
- snapshot ID  

This replaces:

- submodules  
- subtrees  
- monorepo hacks  
- external build systems  

with a native Git solution.

---

### **3.8. Import Path Abstraction Layer**

Introduce:

```
.gitimports
```

Mapping:

- clean semantic names → repository identities  
- repository identities → URLs  

This allows:

- renaming repositories  
- moving repositories  
- reorganizing namespaces  

without breaking source code.

---

### **3.9. Reproducible Multi-Repo Snapshots**

Introduce:

```
.gitproject.lock
```

Containing:

- exact commits for all repos  
- integrity hashes  
- dependency graph hash  

This enables:

- reproducible builds  
- reproducible CI  
- reproducible releases  

across multi-repo systems.

---

## **4. Backward Compatibility**

All proposed files:

- are optional  
- do not affect existing Git behavior  
- do not break existing repositories  
- can be ignored by older Git versions  
- can be adopted incrementally  

This ensures safe adoption.

---

## **5. Benefits**

### **For developers**
- stable naming  
- reproducible builds  
- long-term maintainability  
- clear provenance  
- easier forking  
- easier patch tracking  

### **For large organizations**
- multi-repo project management  
- compliance and auditability  
- security integration  
- deterministic builds  

### **For ecosystems**
- no need to reinvent dependency systems  
- no need to encode URLs into source code  
- no need for fragile submodules  

---

## **6. Conclusion**

Git is an exceptional version-control system, but modern software development requires features that Git does not currently provide. This RFC proposes a set of optional, backward-compatible extensions that would allow Git to evolve into a complete, future-proof foundation for multi-repository, dependency-driven development.

I welcome discussion, critique, and refinement of these ideas.

**— Harald Houppermans**

---

If you want, I can also prepare:

- a shorter version  
- a more formal academic-style version  
- a version targeted at GitHub/GitLab instead of Git itself  
- a version with diagrams and examples



***
333 EXAMPLE SECTION
***

---

# **RFC (with Examples): Enhancing Git with First-Class Dependency, Provenance, and Security Metadata**

**From:** Harald Houppermans  
**Subject:** RFC (with Examples): Missing Git Features for Modern Multi-Repository Development  
**To:** git@vger.kernel.org  
**Date:** (fill in)

---

## **1. Introduction**

Modern software development relies heavily on:

- multi-repository dependency graphs  
- reproducible builds  
- long-term provenance  
- fork synchronization  
- security advisories  
- stable module identities  

Git provides none of these natively.  
This RFC illustrates the missing features using **real examples** from common workflows.

---

# **2. Problems Illustrated with Real Examples**

## **2.1. Missing Dependency Manifest**

### **Example Problem**
A project depends on 40+ upstream repositories:

```
github.com/go-yaml/yaml
github.com/pelletier/go-toml
golang.org/x/crypto
github.com/stretchr/testify
```

Git has **no way** to record:

- why these dependencies exist  
- which versions are required  
- which commits were used  
- how they relate to each other  

Developers must rely on:

- ad-hoc documentation  
- external package managers  
- fragile submodules  

### **Proposed Solution**
Introduce:

```
.gitdeps
```

Example:

```
[yaml]
url = "https://github.com/go-yaml/yaml"
commit = "a3f1234"
purpose = "YAML parsing"

[toml]
url = "https://github.com/pelletier/go-toml"
commit = "b7c9812"
purpose = "TOML configuration"
```

---

## **2.2. Missing Lockfile for Reproducible Builds**

### **Example Problem**
Two developers clone the same project.  
One gets dependency commit A, the other gets commit B.

Builds differ.  
Bugs differ.  
Security exposure differs.

### **Proposed Solution**
Introduce:

```
.gitdeps.lock
```

Example:

```
yaml = "a3f1234"
toml = "b7c9812"
crypto = "c9d8123"
```

This ensures **deterministic builds**.

---

## **2.3. Missing Provenance Metadata**

### **Example Problem**
A forked repository loses its upstream information:

```
git remote add upstream ...
```

This is **local only**.  
Once pushed to GitHub/GitLab, provenance is lost.

### **Proposed Solution**
Introduce:

```
.gitorigin
```

Example:

```
origin = "https://github.com/go-yaml/yaml"
forked_by = "Skybuck"
reason = "Long-term maintenance + reproducibility"
```

This metadata travels with the repository.

---

## **2.4. Missing Stable Repository Identity**

### **Example Problem**
If a repository is renamed:

```
github.com/user1/yaml → github.com/user2/yaml
```

All import paths break.  
All tooling breaks.  
All downstream forks break.

Git treats the URL as the identity.

### **Proposed Solution**
Introduce a stable repository UUID:

```
.git/identity
uuid = "d8f1-9c2e-44b1-8f3a-abc123"
```

URLs can change; identity remains stable.

---

## **2.5. Missing Security Metadata**

### **Example Problem**
A dependency has a CVE:

```
CVE-2022-28948 in go-yaml/yaml
```

Git has no way to:

- warn on checkout  
- warn on merge  
- warn on dependency resolution  

### **Proposed Solution**
Introduce:

```
.gitsecurity
```

Example:

```
[yaml]
cve = ["CVE-2022-28948"]
fixed_in = "v3.0.1"
severity = "high"
```

Git could warn:

> “Warning: dependency yaml@a3f1234 contains known vulnerabilities.”

---

## **2.6. Missing Fork Synchronization Metadata**

### **Example Problem**
A fork diverges from upstream.  
There is no built-in way to track:

- last upstream sync  
- pending upstream commits  
- divergence depth  

### **Proposed Solution**
Introduce:

```
.gitfork
```

Example:

```
upstream = "https://github.com/go-yaml/yaml"
last_synced = "a3f1234"
pending_commits = 12
```

---

## **2.7. Missing Multi-Repository Project Support**

### **Example Problem**
A project consists of 20 repositories.  
Git submodules are:

- fragile  
- drift-prone  
- hard to clone  
- hard to update  
- hard to audit  

### **Proposed Solution**
Introduce:

```
.gitproject
```

Example:

```
repos = [
  "core",
  "parser",
  "crypto",
  "network",
  "ui"
]
```

And a lockfile:

```
.gitproject.lock
core = "a1b2c3"
parser = "d4e5f6"
crypto = "112233"
```

This enables **reproducible multi-repo snapshots**.

---

## **2.8. Missing Import Path Abstraction**

### **Example Problem**
Languages like Go embed URLs directly into source code:

```
import "github.com/go-yaml/yaml"
```

If the repo moves or is renamed, the code breaks.

### **Proposed Solution**
Introduce:

```
.gitimports
```

Example:

```
sky/yaml = "uuid:d8f1-9c2e-44b1-8f3a-abc123"
```

Source code imports:

```
import "sky/yaml"
```

Git resolves it via the identity mapping.

---

# **3. Summary of Proposed Files**

| File | Purpose |
|------|---------|
| `.gitdeps` | Dependency manifest |
| `.gitdeps.lock` | Reproducible dependency snapshot |
| `.gitorigin` | Provenance metadata |
| `.gitsecurity` | Security advisories |
| `.gitfork` | Fork synchronization metadata |
| `.gitproject` | Multi-repo project definition |
| `.gitproject.lock` | Multi-repo snapshot |
| `.gitimports` | Import path abstraction |

All files are:

- optional  
- backward-compatible  
- non-breaking  
- easy to adopt incrementally  

---

# **4. Conclusion**

These examples demonstrate that Git lacks several features required for modern multi-repository, dependency-driven development. The proposed metadata files and identity mechanisms would significantly improve:

- reproducibility  
- provenance  
- security  
- maintainability  
- long-term stability  

I welcome discussion and refinement.

**— Harald Houppermans**

Bye for now,
  Skybuck Flying/Harald Houppermans ! ;) =D XD 

^ permalink raw reply

* Re: [PATCH v1 3/4] environment: move 'trust_executable_bit' into repo_config_values
From: Tian Yuchen @ 2026-06-01 18:03 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, christian.couder, ps, Ayush Chandekar, Olamide Caleb Bello
In-Reply-To: <e0d5b1af-b040-49e2-90f9-d8325682826b@malon.dev>

On 6/1/26 18:10, Tian Yuchen wrote:

> That’s true: I had actually planned to start migrating has_symlinks as 
> soon as this series was approved. Since you think it would be better to 
> merge them into a single series, I’ll go ahead and do that ;)
> 

I’ve found that migrating has_symlinks seems to be quite a tricky 
business. Some callers in certain files pass very few parameters, and 
the call stack is quite deep, if I am correct. so I feel that adding a 
repo for this purpose might be overkill. Perhaps it would be better to 
focus on trust_executable_bit for now?

Regards, yuchen

^ permalink raw reply

* Re: [GSoC][PATCH 3/4] repo: add path.gitdir with absolute and relative suffix formatting
From: Lucas Seiki Oshiro @ 2026-06-01 16:28 UTC (permalink / raw)
  To: K Jayatheerth
  Cc: git, jltobler, gitster, phillip.wood, sandals, kumarayushjha123,
	a3205153416
In-Reply-To: <20260601151950.30686-4-jayatheerthkulkarni2005@gmail.com>


> +test_repo_info_path () {
> + field_name=$1
> + expect_relative=$2
> +
> + test_expect_success "query individual key: path.$field_name.absolute" '
> + (
> + cd test-repo/sub &&
> + expect_absolute=$(cd .. && pwd)/.git &&

Note that this semi-hardcoded path won't work for other values (e.g.
top level dir, superproject working tree). This needs to be a parameter
just like `expect_relative`

^ permalink raw reply

* Re: [GSoC][PATCH 0/4] teach git repo info to handle path keys
From: Lucas Seiki Oshiro @ 2026-06-01 16:25 UTC (permalink / raw)
  To: K Jayatheerth
  Cc: git, jltobler, gitster, phillip.wood, sandals, kumarayushjha123,
	a3205153416
In-Reply-To: <20260601151950.30686-1-jayatheerthkulkarni2005@gmail.com>


> 1. Should there still be a --path-format flag?

If you specify "absolute" and "relative" in the keys, it won't
make sense to use it.

> 2. Should we consider a default option?

Some pros and cons:

- Pro: some values make more sense to be in absolute or relative
  format
- Pro: it's boring to always add `.(relative|absolute)` to the
  paths
- Con: it will be perpetuating what git-rev-parse does, and we
  don't git-repo-info to be git-rev-parse with a different
  interface. It's our chance to learn with [1] for example.
- Con: the user will need if the value is relative or absolute

> 3. Is printing both absolute and relative in a single call
>   using --all acceptable?

If you're providing both keys, I think it's not only acceptable
but mandatory. `--all` should mean "all", not "all, but ...".

> I have discussed these changes with both Justin and Lucas
> internally. This series is presented to gather opinions from the
> wider community before moving forward.

I probably sent the same comments internally, but I'm sending
here to share my opinions with the rest of the community ;-)

[1] fac60b8925 (rev-parse: add option for absolute or relative path formatting, 2020-12-13)



^ permalink raw reply

* [PATCH v2] index-pack: retain child bases in delta cache
From: Arijit Banerjee via GitGitGadget @ 2026-06-01 16:13 UTC (permalink / raw)
  To: git
  Cc: Ævar Arnfjörð Bjarmason, Junio C Hamano,
	Derrick Stolee, Arijit Banerjee, Arijit Banerjee
In-Reply-To: <pull.2131.git.1780070763044.gitgitgadget@gmail.com>

From: Arijit Banerjee <arijit@effectiveailabs.com>

When resolving a delta whose result has children of its own,
index-pack adds the result to work_head, accounts its data in
base_cache_used, and calls prune_base_data(). It then immediately frees
that same data.

This bypasses the existing delta base cache policy and can force later
descendants to reconstruct the queued base again. Let the existing
delta_base_cache_limit pruning policy decide whether to keep or evict
the data instead.

This does not add a new cache or increase the cache limit. The object
data is already accounted in base_cache_used before prune_base_data()
runs, and the existing pruning and base cleanup paths still release it.

On a quiet Ubuntu 24.04 VM with 16 vCPUs, 32 GiB RAM, and local SSD,
direct index-pack timings on single-pack Linux fixtures improved as
follows:

  linux blobless: 69.17s -> 57.98s (16.2% faster), RSS flat
  linux full:     280.72s -> 236.32s (15.8% faster), RSS +1.9%

Five-repeat medians on public repositories also improved:

  git.git:  12.31s -> 10.70s (13.1% faster)
  libgit2:   3.35s ->  2.88s (14.0% faster)
  redis:     6.52s ->  5.64s (13.5% faster)
  cpython:  33.02s -> 31.44s (4.8% faster)

The standard p5302 perf test on a smaller git.git fixture was neutral:

  5302.9 index-pack default threads:
    11.21(38.07+1.33) -> 11.16(37.90+1.31), -0.4%

t/t5302-pack-index.sh passed, and GitGitGadget's linux-leaks CI also
exercised that test under SANITIZE=leak.

Signed-off-by: Arijit Banerjee <arijit@effectiveailabs.com>
---
    index-pack: retain child bases in delta cache
    
    Speed up the local pack indexing phase of clone/fetch for large
    delta-compressed packs by keeping reconstructed delta bases available
    for reuse when they are queued for later delta resolution.
    
    When index-pack reconstructs a child base and queues it for resolving
    descendant deltas, it currently frees that data immediately. This can
    force the same base to be reconstructed again. Instead, keep it in the
    existing delta base cache and let the existing delta_base_cache_limit
    policy decide whether to retain or evict it.
    
    This does not add a new cache or increase the cache limit. The object
    data is already accounted in base_cache_used, and prune_base_data() is
    already called at this point.
    
    Correctness:
    
     * t/t5302-pack-index.sh passed all 36 tests.
    
    Benchmarks on a quiet Ubuntu 24.04 VM, 16 vCPU, 32 GiB RAM, local SSD:
    
    pack baseline patched wall-time change RSS change linux blobless 69.17s
    57.98s 16.2% faster -0.0% linux full 280.72s 236.32s 15.8% faster +1.9%
    
    Five-repeat public-repo medians also improved: git.git 13.1%, libgit2
    14.0%, redis 13.5%, cpython 4.8%.
    
    Perf on the linux blobless pack showed the same direction under
    profiling: 76.64s baseline vs 61.09s patched, with similar RSS.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2131%2Farijit91%2Findex-pack-retain-child-base-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2131/arijit91/index-pack-retain-child-base-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2131

Range-diff vs v1:

 1:  cafb1700be ! 1:  42eca38f51 index-pack: retain child bases in delta cache
     @@ Commit message
      
          When resolving a delta whose result has children of its own,
          index-pack adds the result to work_head, accounts its data in
     -    base_cache_used, and calls prune_base_data(). It then immediately
     -    frees that same data.
     +    base_cache_used, and calls prune_base_data(). It then immediately frees
     +    that same data.
      
          This bypasses the existing delta base cache policy and can force later
          descendants to reconstruct the queued base again. Let the existing
          delta_base_cache_limit pruning policy decide whether to keep or evict
          the data instead.
      
     +    This does not add a new cache or increase the cache limit. The object
     +    data is already accounted in base_cache_used before prune_base_data()
     +    runs, and the existing pruning and base cleanup paths still release it.
     +
     +    On a quiet Ubuntu 24.04 VM with 16 vCPUs, 32 GiB RAM, and local SSD,
     +    direct index-pack timings on single-pack Linux fixtures improved as
     +    follows:
     +
     +      linux blobless: 69.17s -> 57.98s (16.2% faster), RSS flat
     +      linux full:     280.72s -> 236.32s (15.8% faster), RSS +1.9%
     +
     +    Five-repeat medians on public repositories also improved:
     +
     +      git.git:  12.31s -> 10.70s (13.1% faster)
     +      libgit2:   3.35s ->  2.88s (14.0% faster)
     +      redis:     6.52s ->  5.64s (13.5% faster)
     +      cpython:  33.02s -> 31.44s (4.8% faster)
     +
     +    The standard p5302 perf test on a smaller git.git fixture was neutral:
     +
     +      5302.9 index-pack default threads:
     +        11.21(38.07+1.33) -> 11.16(37.90+1.31), -0.4%
     +
     +    t/t5302-pack-index.sh passed, and GitGitGadget's linux-leaks CI also
     +    exercised that test under SANITIZE=leak.
     +
          Signed-off-by: Arijit Banerjee <arijit@effectiveailabs.com>
      
       ## builtin/index-pack.c ##


 builtin/index-pack.c | 1 -
 1 file changed, 1 deletion(-)

diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index cf0bd8280d..027c64b522 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -1212,7 +1212,6 @@ static void *threaded_second_pass(void *data)
 			list_add(&child->list, &work_head);
 			base_cache_used += child->size;
 			prune_base_data(NULL);
-			free_base_data(child);
 		} else if (child) {
 			/*
 			 * This child does not have its own children. It may be

base-commit: c69baaf57ba26cf117c2b6793802877f19738b0d
-- 
gitgitgadget

^ permalink raw reply related

* Re: [PATCH] sub-process: use gentle handshake to avoid die() on startup failure
From: Michael Montalbo @ 2026-06-01 15:51 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Michael Montalbo via GitGitGadget, git
In-Reply-To: <xmqqo6hu92wu.fsf@gitster.g>

On Sun, May 31, 2026 at 11:43 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Michael Montalbo via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > diff --git a/sub-process.c b/sub-process.c
> > index 83bf0a0e82..22c68bd10d 100644
> > --- a/sub-process.c
> > +++ b/sub-process.c
> > @@ -132,18 +132,19 @@ static int handshake_version(struct child_process *process,
> >       if (packet_flush_gently(process->in))
> >               return error("Could not write flush packet");
> >
> > -     if (!(line = packet_read_line(process->out, NULL)) ||
> > +     if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> >           !skip_prefix(line, welcome_prefix, &p) ||
> >           strcmp(p, "-server"))
> >               return error("Unexpected line '%s', expected %s-server",
> >                            line ? line : "<flush packet>", welcome_prefix);
>
> If `packet_read_line_gently()` returns `< 0` (due to an EOF or read
> error), `line` will be `NULL`.  The error message printed will be:
>
>     `Unexpected line '<flush packet>', expected filter-server`
>
> This is misleading when the remote process didn't send a flush
> packet; it hung up or crashed.
>

Makes sense. Will fix.

>
>
> > -     if (!(line = packet_read_line(process->out, NULL)) ||
> > +     if (packet_read_line_gently(process->out, NULL, &line) <= 0 ||
> >           !skip_prefix(line, "version=", &p) ||
> >           strtol_i(p, 10, chosen_version))
> >               return error("Unexpected line '%s', expected version",
> >                            line ? line : "<flush packet>");
>
> Ditto.
>

Will fix.

> > -     if ((line = packet_read_line(process->out, NULL)))
> > -             return error("Unexpected line '%s', expected flush", line);
> > +     if (packet_read_line_gently(process->out, NULL, &line) < 0 || line)
> > +             return error("Unexpected line '%s', expected flush",
> > +                          line ? line : "<read error>");
>
> We catch error return (< 0) or a line with payload (!!line) and
> report an error here, because we want to see <flush> here.  OK.
>
>
> > @@ -171,7 +172,7 @@ static int handshake_capabilities(struct child_process *process,
> >       if (packet_flush_gently(process->in))
> >               return error("Could not write flush packet");
> >
> > -     while ((line = packet_read_line(process->out, NULL))) {
> > +     while (packet_read_line_gently(process->out, NULL, &line) > 0) {
> >               const char *p;
> >               if (!skip_prefix(line, "capability=", &p))
> >                       continue;
>
> While this correctly stops the loop if packet_read_line_gently()
> returns a non-positive value, doesn't it introduce a subtle bug?
>
> `packet_read_line_gently()` returns:
>
>   - `> 0` for a normal line (which keeps the loop running).
>
>   - `0` for a flush packet (which we expect as the normal terminator
>     of the capabilities list, stopping the loop).
>
>   - `< 0` for an EOF or read error (which also stops the loop).
>
> In the original code, an EOF or read error would have caused
> `packet_read_line()` to call `die()`, aborting the process.
>
> With the new code, if the child process dies or closes its pipe
> during the capabilities handshake, the loop will terminate, and the
> function will return `0` (success). The parent process will proceed
> as if the capabilities were successfully negotiated.  Any further
> communication with the child process would fail so the damage may
> not be huge, but somebody must check if the loop terminated because
> of a flush packet, or an error.
>
>         while (1) {
>                 const char *p;
>                 int len = packet_read_line_gently(process->out, NULL, &line);
>
>                 if (len < 0)
>                         return error(_("subprocess `%s` failed to give capabilities"),
>                                 process->args.v[0]);
>                 if (!skip_prefix(line, "capability=", &p))
>                         continue;
>                 ...
>
> or something, perhaps?

Good catch, thank you. I will update the logic so a failure during
capabilities handshake
is correctly marked an error instead of silently succeeding.

^ permalink raw reply

* [PATCH v4 8/8] environment: move "warn_on_object_refname_ambiguity" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.warnAmbiguousRefs` configuration was previously stored in a
global `int` variable, making it shared across repository instances
and risking cross‑repository state leakage.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. This option is parsed eagerly because
ambiguity warnings influence how users interpret object references in
many commands; a lazy parse could cause these warnings to behave
inconsistently or to appear for the wrong repository, confusing users
and hindering libification. This preserves the existing behavior while
tying the value to the repository from which it was read, avoiding
cross‑repository state leakage and continuing the effort to reduce
reliance on global configuration state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 builtin/cat-file.c     | 7 ++++---
 builtin/pack-objects.c | 7 ++++---
 environment.c          | 2 +-
 environment.h          | 2 +-
 object-name.c          | 3 ++-
 revision.c             | 7 ++++---
 submodule.c            | 7 ++++---
 7 files changed, 20 insertions(+), 15 deletions(-)

diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index d9fbad5358..cfc5430186 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -901,6 +901,7 @@ static int batch_objects(struct batch_options *opt)
 	struct strbuf input = STRBUF_INIT;
 	struct strbuf output = STRBUF_INIT;
 	struct expand_data data = EXPAND_DATA_INIT;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	int save_warning;
 	int retval = 0;
 
@@ -973,8 +974,8 @@ static int batch_objects(struct batch_options *opt)
 	 * warn) ends up dwarfing the actual cost of the object lookups
 	 * themselves. We can work around it by just turning off the warning.
 	 */
-	save_warning = warn_on_object_refname_ambiguity;
-	warn_on_object_refname_ambiguity = 0;
+	save_warning = cfg->warn_on_object_refname_ambiguity;
+	cfg->warn_on_object_refname_ambiguity = 0;
 
 	if (opt->batch_mode == BATCH_MODE_QUEUE_AND_DISPATCH) {
 		batch_objects_command(opt, &output, &data);
@@ -1002,7 +1003,7 @@ static int batch_objects(struct batch_options *opt)
  cleanup:
 	strbuf_release(&input);
 	strbuf_release(&output);
-	warn_on_object_refname_ambiguity = save_warning;
+	cfg->warn_on_object_refname_ambiguity = save_warning;
 	return retval;
 }
 
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index 8ccbe7e178..7df75fe91e 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -4788,6 +4788,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
 	struct setup_revision_opt s_r_opt = {
 		.allow_exclude_promisor_objects = 1,
 	};
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	char line[1000];
 	int flags = 0;
 	int save_warning;
@@ -4798,8 +4799,8 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
 	/* make sure shallows are read */
 	is_repository_shallow(the_repository);
 
-	save_warning = warn_on_object_refname_ambiguity;
-	warn_on_object_refname_ambiguity = 0;
+	save_warning = cfg->warn_on_object_refname_ambiguity;
+	cfg->warn_on_object_refname_ambiguity = 0;
 
 	while (fgets(line, sizeof(line), stdin) != NULL) {
 		int len = strlen(line);
@@ -4827,7 +4828,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv)
 			die(_("bad revision '%s'"), line);
 	}
 
-	warn_on_object_refname_ambiguity = save_warning;
+	cfg->warn_on_object_refname_ambiguity = save_warning;
 
 	if (use_bitmap_index && !get_object_list_from_bitmap(revs))
 		return;
diff --git a/environment.c b/environment.c
index 57587ede56..ba2c60103f 100644
--- a/environment.c
+++ b/environment.c
@@ -47,7 +47,6 @@ int minimum_abbrev = 4, default_abbrev = -1;
 int ignore_case;
 int assume_unchanged;
 int is_bare_repository_cfg = -1; /* unspecified */
-int warn_on_object_refname_ambiguity = 1;
 char *git_commit_encoding;
 char *git_log_output_encoding;
 char *apply_default_whitespace;
@@ -725,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 	cfg->core_sparse_checkout_cone = 0;
 	cfg->sparse_expect_files_outside_of_patterns = 0;
+	cfg->warn_on_object_refname_ambiguity = 1;
 }
diff --git a/environment.h b/environment.h
index 609cdaa07f..1ff0a7ba8b 100644
--- a/environment.h
+++ b/environment.h
@@ -97,6 +97,7 @@ struct repo_config_values {
 	int pack_compression_level;
 	int precomposed_unicode;
 	int core_sparse_checkout_cone;
+	int warn_on_object_refname_ambiguity;
 
 	/* section "sparse" config values */
 	int sparse_expect_files_outside_of_patterns;
@@ -174,7 +175,6 @@ extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
 extern int ignore_case;
 extern int assume_unchanged;
-extern int warn_on_object_refname_ambiguity;
 extern char *apply_default_whitespace;
 extern char *apply_default_ignorewhitespace;
 extern unsigned long pack_size_limit_cfg;
diff --git a/object-name.c b/object-name.c
index 21dcdc4a0e..319d3db01d 100644
--- a/object-name.c
+++ b/object-name.c
@@ -684,11 +684,12 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
 	int refs_found = 0;
 	int at, reflog_len, nth_prior = 0;
 	int fatal = !(flags & GET_OID_QUIETLY);
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (len == r->hash_algo->hexsz && !get_oid_hex(str, oid)) {
 		if (!(flags & GET_OID_SKIP_AMBIGUITY_CHECK) &&
 		    repo_settings_get_warn_ambiguous_refs(r) &&
-		    warn_on_object_refname_ambiguity) {
+		    cfg->warn_on_object_refname_ambiguity) {
 			refs_found = repo_dwim_ref(r, str, len, &tmp_oid, &real_ref, 0);
 			if (refs_found > 0) {
 				warning(warn_msg, len, str);
diff --git a/revision.c b/revision.c
index 599b3a66c3..4e7faa7eb1 100644
--- a/revision.c
+++ b/revision.c
@@ -2922,9 +2922,10 @@ static void read_revisions_from_stdin(struct rev_info *revs,
 	int seen_end_of_options = 0;
 	int save_warning;
 	int flags = 0;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	save_warning = warn_on_object_refname_ambiguity;
-	warn_on_object_refname_ambiguity = 0;
+	save_warning = cfg->warn_on_object_refname_ambiguity;
+	cfg->warn_on_object_refname_ambiguity = 0;
 
 	strbuf_init(&sb, 1000);
 	while (strbuf_getline(&sb, stdin) != EOF) {
@@ -2958,7 +2959,7 @@ static void read_revisions_from_stdin(struct rev_info *revs,
 		read_pathspec_from_stdin(&sb, prune);
 
 	strbuf_release(&sb);
-	warn_on_object_refname_ambiguity = save_warning;
+	cfg->warn_on_object_refname_ambiguity = save_warning;
 }
 
 static void NORETURN diagnose_missing_default(const char *def)
diff --git a/submodule.c b/submodule.c
index b1a0363f9d..f26235bbb7 100644
--- a/submodule.c
+++ b/submodule.c
@@ -898,12 +898,13 @@ static void collect_changed_submodules(struct repository *r,
 	struct setup_revision_opt s_r_opt = {
 		.assume_dashdash = 1,
 	};
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	save_warning = warn_on_object_refname_ambiguity;
-	warn_on_object_refname_ambiguity = 0;
+	save_warning = cfg->warn_on_object_refname_ambiguity;
+	cfg->warn_on_object_refname_ambiguity = 0;
 	repo_init_revisions(r, &rev, NULL);
 	setup_revisions_from_strvec(argv, &rev, &s_r_opt);
-	warn_on_object_refname_ambiguity = save_warning;
+	cfg->warn_on_object_refname_ambiguity = save_warning;
 	if (prepare_revision_walk(&rev))
 		die(_("revision walk setup failed"));
 
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 7/8] environment: move "sparse_expect_files_outside_of_patterns" into `repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.sparseCheckoutExpectFilesOutsideOfPatterns` configuration was
previously stored in a global `int` variable, making it shared across
repository instances and risking cross‑repository state leakage.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. This option is parsed eagerly because
it controls how sparse‑checkout paths are interpreted – a fundamental
behavior that many commands rely on; a lazy parse could cause
inconsistent sparse‑checkout handling and complicate libification.
This preserves the existing behavior while tying the value to the
repository from which it was read, avoiding cross‑repository state
leakage and continuing the effort to reduce reliance on global
configuration state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 environment.c  | 6 ++++--
 environment.h  | 5 +++--
 sparse-index.c | 2 +-
 3 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/environment.c b/environment.c
index b0e873e9f5..57587ede56 100644
--- a/environment.c
+++ b/environment.c
@@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
 #endif
 enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
 int grafts_keep_true_parents;
-int sparse_expect_files_outside_of_patterns;
 unsigned long pack_size_limit_cfg;
 
 #ifndef PROTECT_HFS_DEFAULT
@@ -550,8 +549,10 @@ int git_default_core_config(const char *var, const char *value,
 
 static int git_default_sparse_config(const char *var, const char *value)
 {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
 	if (!strcmp(var, "sparse.expectfilesoutsideofpatterns")) {
-		sparse_expect_files_outside_of_patterns = git_config_bool(var, value);
+		cfg->sparse_expect_files_outside_of_patterns = git_config_bool(var, value);
 		return 0;
 	}
 
@@ -723,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
 	cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 	cfg->core_sparse_checkout_cone = 0;
+	cfg->sparse_expect_files_outside_of_patterns = 0;
 }
diff --git a/environment.h b/environment.h
index befad9a388..609cdaa07f 100644
--- a/environment.h
+++ b/environment.h
@@ -98,6 +98,9 @@ struct repo_config_values {
 	int precomposed_unicode;
 	int core_sparse_checkout_cone;
 
+	/* section "sparse" config values */
+	int sparse_expect_files_outside_of_patterns;
+
 	/* section "branch" config values */
 	enum branch_track branch_track;
 };
@@ -179,8 +182,6 @@ extern unsigned long pack_size_limit_cfg;
 extern int protect_hfs;
 extern int protect_ntfs;
 
-extern int sparse_expect_files_outside_of_patterns;
-
 enum rebase_setup_type {
 	AUTOREBASE_NEVER = 0,
 	AUTOREBASE_LOCAL,
diff --git a/sparse-index.c b/sparse-index.c
index 53cb8d64fc..1ed769b78d 100644
--- a/sparse-index.c
+++ b/sparse-index.c
@@ -675,7 +675,7 @@ void clear_skip_worktree_from_present_files(struct index_state *istate)
 	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (!cfg->apply_sparse_checkout ||
-	    sparse_expect_files_outside_of_patterns)
+	    cfg->sparse_expect_files_outside_of_patterns)
 		return;
 
 	if (clear_skip_worktree_from_present_files_sparse(istate)) {
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 6/8] environment: move "core_sparse_checkout_cone" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.sparseCheckoutCone` configuration was previously stored in an
uninitialized global `int` variable, risking cross‑repository state
leakage.

Move it into `repo_config_values`, where eagerly‑parsed repository
configuration lives. `core.sparseCheckoutCone` is parsed eagerly
because it determines the fundamental sparse‑checkout mode and is
consulted very early during repository setup; a lazy parse could
leave the sparse‑checkout state undefined and complicate
libification. This preserves the existing behavior while tying the
value to the repository from which it was read, avoiding cross‑
repository state leakage and continuing the effort to reduce reliance
on global configuration state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 builtin/mv.c              |  2 +-
 builtin/sparse-checkout.c | 37 ++++++++++++++++++++++---------------
 dir.c                     |  3 ++-
 environment.c             |  4 ++--
 environment.h             |  2 +-
 sparse-index.c            |  2 +-
 6 files changed, 29 insertions(+), 21 deletions(-)

diff --git a/builtin/mv.c b/builtin/mv.c
index 2215d34e31..ef3a326c90 100644
--- a/builtin/mv.c
+++ b/builtin/mv.c
@@ -574,7 +574,7 @@ int cmd_mv(int argc,
 
 		if (ignore_sparse &&
 		    cfg->apply_sparse_checkout &&
-		    core_sparse_checkout_cone) {
+		    cfg->core_sparse_checkout_cone) {
 			/*
 			 * NEEDSWORK: we are *not* paying attention to
 			 * "out-to-out" move (<source> is out-of-cone and
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index f4aa405da9..92d017b81f 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -73,7 +73,7 @@ static int sparse_checkout_list(int argc, const char **argv, const char *prefix,
 
 	memset(&pl, 0, sizeof(pl));
 
-	pl.use_cone_patterns = core_sparse_checkout_cone;
+	pl.use_cone_patterns = cfg->core_sparse_checkout_cone;
 
 	sparse_filename = get_sparse_checkout_filename();
 	res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0);
@@ -334,6 +334,7 @@ static int write_patterns_and_update(struct repository *repo,
 	FILE *fp;
 	struct lock_file lk = LOCK_INIT;
 	int result;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	sparse_filename = get_sparse_checkout_filename();
 
@@ -353,7 +354,7 @@ static int write_patterns_and_update(struct repository *repo,
 	if (!fp)
 		die_errno(_("unable to fdopen %s"), get_lock_file_path(&lk));
 
-	if (core_sparse_checkout_cone)
+	if (cfg->core_sparse_checkout_cone)
 		write_cone_to_file(fp, pl);
 	else
 		write_patterns_to_file(fp, pl);
@@ -402,15 +403,15 @@ static enum sparse_checkout_mode update_cone_mode(int *cone_mode) {
 
 	/* If not specified, use previous definition of cone mode */
 	if (*cone_mode == -1 && cfg->apply_sparse_checkout)
-		*cone_mode = core_sparse_checkout_cone;
+		*cone_mode = cfg->core_sparse_checkout_cone;
 
 	/* Set cone/non-cone mode appropriately */
 	cfg->apply_sparse_checkout = 1;
 	if (*cone_mode == 1 || *cone_mode == -1) {
-		core_sparse_checkout_cone = 1;
+		cfg->core_sparse_checkout_cone = 1;
 		return MODE_CONE_PATTERNS;
 	}
-	core_sparse_checkout_cone = 0;
+	cfg->core_sparse_checkout_cone = 0;
 	return MODE_ALL_PATTERNS;
 }
 
@@ -577,7 +578,9 @@ static void add_patterns_from_input(struct pattern_list *pl,
 				    FILE *file)
 {
 	int i;
-	if (core_sparse_checkout_cone) {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
+	if (cfg->core_sparse_checkout_cone) {
 		struct strbuf line = STRBUF_INIT;
 
 		hashmap_init(&pl->recursive_hashmap, pl_hashmap_cmp, NULL, 0);
@@ -636,13 +639,14 @@ static void add_patterns_cone_mode(int argc, const char **argv,
 	struct pattern_entry *pe;
 	struct hashmap_iter iter;
 	struct pattern_list existing;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	char *sparse_filename = get_sparse_checkout_filename();
 
 	add_patterns_from_input(pl, argc, argv,
 				use_stdin ? stdin : NULL);
 
 	memset(&existing, 0, sizeof(existing));
-	existing.use_cone_patterns = core_sparse_checkout_cone;
+	existing.use_cone_patterns = cfg->core_sparse_checkout_cone;
 
 	if (add_patterns_from_file_to_list(sparse_filename, "", 0,
 					   &existing, NULL, 0))
@@ -690,7 +694,7 @@ static int modify_pattern_list(struct repository *repo,
 
 	switch (m) {
 	case ADD:
-		if (core_sparse_checkout_cone)
+		if (cfg->core_sparse_checkout_cone)
 			add_patterns_cone_mode(args->nr, args->v, pl, use_stdin);
 		else
 			add_patterns_literal(args->nr, args->v, pl, use_stdin);
@@ -723,11 +727,12 @@ static void sanitize_paths(struct repository *repo,
 			   const char *prefix, int skip_checks)
 {
 	int i;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (!args->nr)
 		return;
 
-	if (prefix && *prefix && core_sparse_checkout_cone) {
+	if (prefix && *prefix && cfg->core_sparse_checkout_cone) {
 		/*
 		 * The args are not pathspecs, so unfortunately we
 		 * cannot imitate how cmd_add() uses parse_pathspec().
@@ -744,10 +749,10 @@ static void sanitize_paths(struct repository *repo,
 	if (skip_checks)
 		return;
 
-	if (prefix && *prefix && !core_sparse_checkout_cone)
+	if (prefix && *prefix && !cfg->core_sparse_checkout_cone)
 		die(_("please run from the toplevel directory in non-cone mode"));
 
-	if (core_sparse_checkout_cone) {
+	if (cfg->core_sparse_checkout_cone) {
 		for (i = 0; i < args->nr; i++) {
 			if (args->v[i][0] == '/')
 				die(_("specify directories rather than patterns (no leading slash)"));
@@ -769,7 +774,7 @@ static void sanitize_paths(struct repository *repo,
 		if (S_ISSPARSEDIR(ce->ce_mode))
 			continue;
 
-		if (core_sparse_checkout_cone)
+		if (cfg->core_sparse_checkout_cone)
 			die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), args->v[i]);
 		else
 			warning(_("pass a leading slash before paths such as '%s' if you want a single file (see NON-CONE PROBLEMS in the git-sparse-checkout manual)."), args->v[i]);
@@ -836,6 +841,7 @@ static struct sparse_checkout_set_opts {
 static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
 			       struct repository *repo)
 {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	int default_patterns_nr = 2;
 	const char *default_patterns[] = {"/*", "!/*/", NULL};
 
@@ -873,7 +879,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix,
 	 * non-cone mode, if nothing is specified, manually select just the
 	 * top-level directory (much as 'init' would do).
 	 */
-	if (!core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) {
+	if (!cfg->core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) {
 		for (int i = 0; i < default_patterns_nr; i++)
 			strvec_push(&patterns, default_patterns[i]);
 	} else {
@@ -977,7 +983,7 @@ static int sparse_checkout_clean(int argc, const char **argv,
 	setup_work_tree();
 	if (!cfg->apply_sparse_checkout)
 		die(_("must be in a sparse-checkout to clean directories"));
-	if (!core_sparse_checkout_cone)
+	if (!cfg->core_sparse_checkout_cone)
 		die(_("must be in a cone-mode sparse-checkout to clean directories"));
 
 	argc = parse_options(argc, argv, prefix,
@@ -1141,6 +1147,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char *
 	FILE *fp;
 	int ret;
 	struct pattern_list pl = {0};
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	char *sparse_filename;
 	check_rules_opts.cone_mode = -1;
 
@@ -1152,7 +1159,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char *
 		check_rules_opts.cone_mode = 1;
 
 	update_cone_mode(&check_rules_opts.cone_mode);
-	pl.use_cone_patterns = core_sparse_checkout_cone;
+	pl.use_cone_patterns = cfg->core_sparse_checkout_cone;
 	if (check_rules_opts.rules_file) {
 		fp = xfopen(check_rules_opts.rules_file, "r");
 		add_patterns_from_input(&pl, argc, argv, fp);
diff --git a/dir.c b/dir.c
index fcb8f6dd2a..4f493b64c6 100644
--- a/dir.c
+++ b/dir.c
@@ -3508,8 +3508,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl)
 {
 	int res;
 	char *sparse_filename = get_sparse_checkout_filename();
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	pl->use_cone_patterns = core_sparse_checkout_cone;
+	pl->use_cone_patterns = cfg->core_sparse_checkout_cone;
 	res = add_patterns_from_file_to_list(sparse_filename, "", 0, pl, NULL, 0);
 
 	free(sparse_filename);
diff --git a/environment.c b/environment.c
index 739b647ebe..b0e873e9f5 100644
--- a/environment.c
+++ b/environment.c
@@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED;
 #endif
 enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
 int grafts_keep_true_parents;
-int core_sparse_checkout_cone;
 int sparse_expect_files_outside_of_patterns;
 unsigned long pack_size_limit_cfg;
 
@@ -526,7 +525,7 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.sparsecheckoutcone")) {
-		core_sparse_checkout_cone = git_config_bool(var, value);
+		cfg->core_sparse_checkout_cone = git_config_bool(var, value);
 		return 0;
 	}
 
@@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->zlib_compression_level = Z_BEST_SPEED;
 	cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
 	cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
+	cfg->core_sparse_checkout_cone = 0;
 }
diff --git a/environment.h b/environment.h
index 508cb1afbc..befad9a388 100644
--- a/environment.h
+++ b/environment.h
@@ -96,6 +96,7 @@ struct repo_config_values {
 	int zlib_compression_level;
 	int pack_compression_level;
 	int precomposed_unicode;
+	int core_sparse_checkout_cone;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -178,7 +179,6 @@ extern unsigned long pack_size_limit_cfg;
 extern int protect_hfs;
 extern int protect_ntfs;
 
-extern int core_sparse_checkout_cone;
 extern int sparse_expect_files_outside_of_patterns;
 
 enum rebase_setup_type {
diff --git a/sparse-index.c b/sparse-index.c
index 13629c075d..53cb8d64fc 100644
--- a/sparse-index.c
+++ b/sparse-index.c
@@ -154,7 +154,7 @@ int is_sparse_index_allowed(struct index_state *istate, int flags)
 {
 	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	if (!cfg->apply_sparse_checkout || !core_sparse_checkout_cone)
+	if (!cfg->apply_sparse_checkout || !cfg->core_sparse_checkout_cone)
 		return 0;
 
 	if (!(flags & SPARSE_INDEX_MEMORY_ONLY)) {
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 5/8] environment: move "precomposed_unicode" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.precomposeunicode` configuration is currently stored in the
global variable `precomposed_unicode`, which makes it shared across
repository instances within a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.precomposeunicode` is parsed
eagerly because it controls Unicode path normalization on macOS,
a fundamental filesystem‑level behavior that many operations depend
on; a lazy parse could lead to inconsistent results and hamper
libification. This preserves the existing behavior while tying the
value to the repository from which it was read, avoiding cross‑
repository state leakage and continuing the effort to reduce reliance
on global configuration state.

Change the type of the field from `int` to `bool` since it is parsed
as a boolean value.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 compat/precompose_utf8.c | 20 +++++++++++++-------
 environment.c            |  4 ++--
 environment.h            |  2 +-
 upload-pack.c            |  3 ++-
 4 files changed, 18 insertions(+), 11 deletions(-)

diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c
index 43b3be0114..0e94dbd862 100644
--- a/compat/precompose_utf8.c
+++ b/compat/precompose_utf8.c
@@ -48,16 +48,18 @@ void probe_utf8_pathname_composition(void)
 	static const char *auml_nfc = "\xc3\xa4";
 	static const char *auml_nfd = "\x61\xcc\x88";
 	int output_fd;
-	if (precomposed_unicode != -1)
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
+	if (cfg->precomposed_unicode != -1)
 		return; /* We found it defined in the global config, respect it */
 	repo_git_path_replace(the_repository, &path, "%s", auml_nfc);
 	output_fd = open(path.buf, O_CREAT|O_EXCL|O_RDWR, 0600);
 	if (output_fd >= 0) {
 		close(output_fd);
 		repo_git_path_replace(the_repository, &path, "%s", auml_nfd);
-		precomposed_unicode = access(path.buf, R_OK) ? 0 : 1;
+		cfg->precomposed_unicode = access(path.buf, R_OK) ? 0 : 1;
 		repo_config_set(the_repository, "core.precomposeunicode",
-				precomposed_unicode ? "true" : "false");
+				cfg->precomposed_unicode ? "true" : "false");
 		repo_git_path_replace(the_repository, &path, "%s", auml_nfc);
 		if (unlink(path.buf))
 			die_errno(_("failed to unlink '%s'"), path.buf);
@@ -69,14 +71,16 @@ const char *precompose_string_if_needed(const char *in)
 {
 	size_t inlen;
 	size_t outlen;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
 	if (!in)
 		return NULL;
 	if (has_non_ascii(in, (size_t)-1, &inlen)) {
 		iconv_t ic_prec;
 		char *out;
-		if (precomposed_unicode < 0)
-			repo_config_get_bool(the_repository, "core.precomposeunicode", &precomposed_unicode);
-		if (precomposed_unicode != 1)
+		if (cfg->precomposed_unicode < 0)
+			repo_config_get_bool(the_repository, "core.precomposeunicode", &cfg->precomposed_unicode);
+		if (cfg->precomposed_unicode != 1)
 			return in;
 		ic_prec = iconv_open(repo_encoding, path_encoding);
 		if (ic_prec == (iconv_t) -1)
@@ -130,7 +134,9 @@ PREC_DIR *precompose_utf8_opendir(const char *dirname)
 
 struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
 {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	struct dirent *res;
+
 	res = readdir(prec_dir->dirp);
 	if (res) {
 		size_t namelenz = strlen(res->d_name) + 1; /* \0 */
@@ -149,7 +155,7 @@ struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir)
 		prec_dir->dirent_nfc->d_ino  = res->d_ino;
 		prec_dir->dirent_nfc->d_type = res->d_type;
 
-		if ((precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) {
+		if ((cfg->precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) {
 			if (prec_dir->ic_precompose == (iconv_t)-1) {
 				die("iconv_open(%s,%s) failed, but needed:\n"
 						"    precomposed unicode is not supported.\n"
diff --git a/environment.c b/environment.c
index d0d3a4b7d2..739b647ebe 100644
--- a/environment.c
+++ b/environment.c
@@ -72,7 +72,6 @@ enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE;
 int grafts_keep_true_parents;
 int core_sparse_checkout_cone;
 int sparse_expect_files_outside_of_patterns;
-int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 unsigned long pack_size_limit_cfg;
 
 #ifndef PROTECT_HFS_DEFAULT
@@ -532,7 +531,7 @@ int git_default_core_config(const char *var, const char *value,
 	}
 
 	if (!strcmp(var, "core.precomposeunicode")) {
-		precomposed_unicode = git_config_bool(var, value);
+		cfg->precomposed_unicode = git_config_bool(var, value);
 		return 0;
 	}
 
@@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->check_stat = 1;
 	cfg->zlib_compression_level = Z_BEST_SPEED;
 	cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
+	cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 }
diff --git a/environment.h b/environment.h
index 514576b67a..508cb1afbc 100644
--- a/environment.h
+++ b/environment.h
@@ -95,6 +95,7 @@ struct repo_config_values {
 	int check_stat;
 	int zlib_compression_level;
 	int pack_compression_level;
+	int precomposed_unicode;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -174,7 +175,6 @@ extern char *apply_default_whitespace;
 extern char *apply_default_ignorewhitespace;
 extern unsigned long pack_size_limit_cfg;
 
-extern int precomposed_unicode;
 extern int protect_hfs;
 extern int protect_ntfs;
 
diff --git a/upload-pack.c b/upload-pack.c
index 9f6d6fe48c..3a52237134 100644
--- a/upload-pack.c
+++ b/upload-pack.c
@@ -1336,6 +1336,7 @@ static int upload_pack_config(const char *var, const char *value,
 			      void *cb_data)
 {
 	struct upload_pack_data *data = cb_data;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (!strcmp("uploadpack.allowtipsha1inwant", var)) {
 		if (git_config_bool(var, value))
@@ -1366,7 +1367,7 @@ static int upload_pack_config(const char *var, const char *value,
 		if (value)
 			data->allow_packfile_uris = 1;
 	} else if (!strcmp("core.precomposeunicode", var)) {
-		precomposed_unicode = git_config_bool(var, value);
+		cfg->precomposed_unicode = git_config_bool(var, value);
 	} else if (!strcmp("transfer.advertisesid", var)) {
 		data->advertise_sid = git_config_bool(var, value);
 	}
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 4/8] environment: move "pack_compression_level" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `pack_compression_level` configuration is currently stored in the
global variable `pack_compression_level`, which makes it shared across
repository instances within a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `pack_compression_level` is parsed
eagerly because it influences packfile compression, a core operation
where a lazy parse could cause inconsistent behavior and hamper
libification. This preserves the existing eager‑parsing behavior while
tying the value to the repository from which it was read, avoiding
cross‑repository state leakage and continuing the effort to reduce
reliance on global configuration state.

The type remains `int` as it represents a numeric compression level,
not a boolean toggle.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 builtin/fast-import.c  |  8 +++++---
 builtin/pack-objects.c | 17 ++++++++++-------
 environment.c          |  8 +++++---
 environment.h          |  2 +-
 object-file.c          |  3 ++-
 5 files changed, 23 insertions(+), 15 deletions(-)

diff --git a/builtin/fast-import.c b/builtin/fast-import.c
index 82bc6dcc00..070a5af3e4 100644
--- a/builtin/fast-import.c
+++ b/builtin/fast-import.c
@@ -965,6 +965,7 @@ static int store_object(
 	unsigned long hdrlen, deltalen;
 	struct git_hash_ctx c;
 	git_zstream s;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	hdrlen = format_object_header((char *)hdr, sizeof(hdr), type,
 				      dat->len);
@@ -1005,7 +1006,7 @@ static int store_object(
 	} else
 		delta = NULL;
 
-	git_deflate_init(&s, pack_compression_level);
+	git_deflate_init(&s, cfg->pack_compression_level);
 	if (delta) {
 		s.next_in = delta;
 		s.avail_in = deltalen;
@@ -1032,7 +1033,7 @@ static int store_object(
 		if (delta) {
 			FREE_AND_NULL(delta);
 
-			git_deflate_init(&s, pack_compression_level);
+			git_deflate_init(&s, cfg->pack_compression_level);
 			s.next_in = (void *)dat->buf;
 			s.avail_in = dat->len;
 			s.avail_out = git_deflate_bound(&s, s.avail_in);
@@ -1115,6 +1116,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
 	struct git_hash_ctx c;
 	git_zstream s;
 	struct hashfile_checkpoint checkpoint;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 	int status = Z_OK;
 
 	/* Determine if we should auto-checkpoint. */
@@ -1134,7 +1136,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark)
 
 	crc32_begin(pack_file);
 
-	git_deflate_init(&s, pack_compression_level);
+	git_deflate_init(&s, cfg->pack_compression_level);
 
 	hdrlen = encode_in_pack_object_header(out_buf, out_sz, OBJ_BLOB, len);
 
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index dd2480a73d..8ccbe7e178 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -386,8 +386,9 @@ static unsigned long do_compress(void **pptr, unsigned long size)
 	git_zstream stream;
 	void *in, *out;
 	unsigned long maxsize;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	git_deflate_init(&stream, pack_compression_level);
+	git_deflate_init(&stream, cfg->pack_compression_level);
 	maxsize = git_deflate_bound(&stream, size);
 
 	in = *pptr;
@@ -413,8 +414,9 @@ static unsigned long write_large_blob_data(struct odb_read_stream *st, struct ha
 	unsigned char ibuf[1024 * 16];
 	unsigned char obuf[1024 * 16];
 	unsigned long olen = 0;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	git_deflate_init(&stream, pack_compression_level);
+	git_deflate_init(&stream, cfg->pack_compression_level);
 
 	for (;;) {
 		ssize_t readlen;
@@ -5003,6 +5005,7 @@ int cmd_pack_objects(int argc,
 	struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
 	struct list_objects_filter_options filter_options =
 		LIST_OBJECTS_FILTER_INIT;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	struct option pack_objects_options[] = {
 		OPT_CALLBACK_F('q', "quiet", &progress, NULL,
@@ -5084,7 +5087,7 @@ int cmd_pack_objects(int argc,
 			 N_("ignore packs that have companion .keep file")),
 		OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
 				N_("ignore this pack")),
-		OPT_INTEGER(0, "compression", &pack_compression_level,
+		OPT_INTEGER(0, "compression", &cfg->pack_compression_level,
 			    N_("pack compression level")),
 		OPT_BOOL(0, "keep-true-parents", &grafts_keep_true_parents,
 			 N_("do not hide commits by grafts")),
@@ -5243,10 +5246,10 @@ int cmd_pack_objects(int argc,
 
 	if (!reuse_object)
 		reuse_delta = 0;
-	if (pack_compression_level == -1)
-		pack_compression_level = Z_DEFAULT_COMPRESSION;
-	else if (pack_compression_level < 0 || pack_compression_level > Z_BEST_COMPRESSION)
-		die(_("bad pack compression level %d"), pack_compression_level);
+	if (cfg->pack_compression_level == -1)
+		cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
+	else if (cfg->pack_compression_level < 0 || cfg->pack_compression_level > Z_BEST_COMPRESSION)
+		die(_("bad pack compression level %d"), cfg->pack_compression_level);
 
 	if (!delta_search_threads)	/* --threads=0 means autodetect */
 		delta_search_threads = online_cpus();
diff --git a/environment.c b/environment.c
index 5b0e88b65c..d0d3a4b7d2 100644
--- a/environment.c
+++ b/environment.c
@@ -52,7 +52,6 @@ char *git_commit_encoding;
 char *git_log_output_encoding;
 char *apply_default_whitespace;
 char *apply_default_ignorewhitespace;
-int pack_compression_level = Z_DEFAULT_COMPRESSION;
 int fsync_object_files = -1;
 int use_fsync = -1;
 enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT;
@@ -390,7 +389,7 @@ int git_default_core_config(const char *var, const char *value,
 		if (!zlib_compression_seen)
 			cfg->zlib_compression_level = level;
 		if (!pack_compression_seen)
-			pack_compression_level = level;
+			cfg->pack_compression_level = level;
 		return 0;
 	}
 
@@ -662,6 +661,8 @@ static int git_default_attr_config(const char *var, const char *value)
 int git_default_config(const char *var, const char *value,
 		       const struct config_context *ctx, void *cb)
 {
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+
 	if (starts_with(var, "core."))
 		return git_default_core_config(var, value, ctx, cb);
 
@@ -701,7 +702,7 @@ int git_default_config(const char *var, const char *value,
 			level = Z_DEFAULT_COMPRESSION;
 		else if (level < 0 || level > Z_BEST_COMPRESSION)
 			die(_("bad pack compression level %d"), level);
-		pack_compression_level = level;
+		cfg->pack_compression_level = level;
 		pack_compression_seen = 1;
 		return 0;
 	}
@@ -721,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->trust_ctime = 1;
 	cfg->check_stat = 1;
 	cfg->zlib_compression_level = Z_BEST_SPEED;
+	cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
 }
diff --git a/environment.h b/environment.h
index 93201620af..514576b67a 100644
--- a/environment.h
+++ b/environment.h
@@ -94,6 +94,7 @@ struct repo_config_values {
 	int trust_ctime;
 	int check_stat;
 	int zlib_compression_level;
+	int pack_compression_level;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -171,7 +172,6 @@ extern int assume_unchanged;
 extern int warn_on_object_refname_ambiguity;
 extern char *apply_default_whitespace;
 extern char *apply_default_ignorewhitespace;
-extern int pack_compression_level;
 extern unsigned long pack_size_limit_cfg;
 
 extern int precomposed_unicode;
diff --git a/object-file.c b/object-file.c
index 7c122ac419..37def5cc59 100644
--- a/object-file.c
+++ b/object-file.c
@@ -1437,8 +1437,9 @@ static int stream_blob_to_pack(struct transaction_packfile *state,
 	int status = Z_OK;
 	int write_object = (flags & INDEX_WRITE_OBJECT);
 	off_t offset = 0;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	git_deflate_init(&s, pack_compression_level);
+	git_deflate_init(&s, cfg->pack_compression_level);
 
 	hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), OBJ_BLOB, size);
 	s.next_out = obuf + hdrlen;
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 3/8] environment: move `zlib_compression_level` into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `zlib_compression_level` configuration is currently stored in the
global variable `zlib_compression_level`, which makes it shared across
repository instances within a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `zlib_compression_level` is parsed
eagerly because it determines compression behaviour for objects and
packs – core operations where a lazy parse could lead to unpredictable
results and hinder libification. This preserves the existing
eager‑parsing behavior while tying the value to the repository it
was read from, avoiding cross‑repository state leakage and continuing
the effort to reduce reliance on global configuration state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 builtin/index-pack.c | 3 ++-
 diff.c               | 3 ++-
 environment.c        | 6 +++---
 environment.h        | 2 +-
 http-push.c          | 3 ++-
 object-file.c        | 3 ++-
 6 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/builtin/index-pack.c b/builtin/index-pack.c
index ca7784dc2c..3942d3e0d0 100644
--- a/builtin/index-pack.c
+++ b/builtin/index-pack.c
@@ -1416,8 +1416,9 @@ static int write_compressed(struct hashfile *f, void *in, unsigned int size)
 	git_zstream stream;
 	int status;
 	unsigned char outbuf[4096];
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	git_deflate_init(&stream, zlib_compression_level);
+	git_deflate_init(&stream, cfg->zlib_compression_level);
 	stream.next_in = in;
 	stream.avail_in = size;
 
diff --git a/diff.c b/diff.c
index 397e38b41c..7d17b0bf3f 100644
--- a/diff.c
+++ b/diff.c
@@ -3589,8 +3589,9 @@ static unsigned char *deflate_it(char *data,
 	int bound;
 	unsigned char *deflated;
 	git_zstream stream;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
-	git_deflate_init(&stream, zlib_compression_level);
+	git_deflate_init(&stream, cfg->zlib_compression_level);
 	bound = git_deflate_bound(&stream, size);
 	deflated = xmalloc(bound);
 	stream.next_out = deflated;
diff --git a/environment.c b/environment.c
index 8542ac3141..5b0e88b65c 100644
--- a/environment.c
+++ b/environment.c
@@ -52,7 +52,6 @@ char *git_commit_encoding;
 char *git_log_output_encoding;
 char *apply_default_whitespace;
 char *apply_default_ignorewhitespace;
-int zlib_compression_level = Z_BEST_SPEED;
 int pack_compression_level = Z_DEFAULT_COMPRESSION;
 int fsync_object_files = -1;
 int use_fsync = -1;
@@ -377,7 +376,7 @@ int git_default_core_config(const char *var, const char *value,
 			level = Z_DEFAULT_COMPRESSION;
 		else if (level < 0 || level > Z_BEST_COMPRESSION)
 			die(_("bad zlib compression level %d"), level);
-		zlib_compression_level = level;
+		cfg->zlib_compression_level = level;
 		zlib_compression_seen = 1;
 		return 0;
 	}
@@ -389,7 +388,7 @@ int git_default_core_config(const char *var, const char *value,
 		else if (level < 0 || level > Z_BEST_COMPRESSION)
 			die(_("bad zlib compression level %d"), level);
 		if (!zlib_compression_seen)
-			zlib_compression_level = level;
+			cfg->zlib_compression_level = level;
 		if (!pack_compression_seen)
 			pack_compression_level = level;
 		return 0;
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
 	cfg->trust_ctime = 1;
 	cfg->check_stat = 1;
+	cfg->zlib_compression_level = Z_BEST_SPEED;
 }
diff --git a/environment.h b/environment.h
index 1d3e2e4f23..93201620af 100644
--- a/environment.h
+++ b/environment.h
@@ -93,6 +93,7 @@ struct repo_config_values {
 	int apply_sparse_checkout;
 	int trust_ctime;
 	int check_stat;
+	int zlib_compression_level;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -170,7 +171,6 @@ extern int assume_unchanged;
 extern int warn_on_object_refname_ambiguity;
 extern char *apply_default_whitespace;
 extern char *apply_default_ignorewhitespace;
-extern int zlib_compression_level;
 extern int pack_compression_level;
 extern unsigned long pack_size_limit_cfg;
 
diff --git a/http-push.c b/http-push.c
index d143fe2845..8ac107a56e 100644
--- a/http-push.c
+++ b/http-push.c
@@ -369,13 +369,14 @@ static void start_put(struct transfer_request *request)
 	int hdrlen;
 	ssize_t size;
 	git_zstream stream;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	unpacked = odb_read_object(the_repository->objects, &request->obj->oid,
 				   &type, &len);
 	hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
 
 	/* Set it up */
-	git_deflate_init(&stream, zlib_compression_level);
+	git_deflate_init(&stream, cfg->zlib_compression_level);
 	size = git_deflate_bound(&stream, len + hdrlen);
 	strbuf_grow(&request->buffer.buf, size);
 	request->buffer.posn = 0;
diff --git a/object-file.c b/object-file.c
index 2acc9522df..7c122ac419 100644
--- a/object-file.c
+++ b/object-file.c
@@ -906,6 +906,7 @@ static int start_loose_object_common(struct odb_source *source,
 	const struct git_hash_algo *algo = source->odb->repo->hash_algo;
 	const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
 	int fd;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	fd = create_tmpfile(source->odb->repo, tmp_file, filename);
 	if (fd < 0) {
@@ -921,7 +922,7 @@ static int start_loose_object_common(struct odb_source *source,
 	}
 
 	/*  Setup zlib stream for compression */
-	git_deflate_init(stream, zlib_compression_level);
+	git_deflate_init(stream, cfg->zlib_compression_level);
 	stream->next_out = buf;
 	stream->avail_out = buflen;
 	algo->init_fn(c);
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 2/8] environment: move "check_stat" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.checkstat` configuration is currently stored in the global
variable `check_stat`, which makes it shared across repository
instances within a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.checkstat` is parsed eagerly
because it controls how `match_stat_data()` and related functions
decide file freshness; a lazy parse could lead to unexpected
behavior or complicate libification. This preserves the existing
eager‑parsing behavior while tying the value to the repository it
was read from, avoiding cross‑repository state leakage, and
continuing the effort to reduce reliance on global configuration
state.

Update all references to use `repo_config_values()`.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Usman Akinyemi <usmanakinyemi202@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 entry.c       |  3 ++-
 environment.c |  6 +++---
 environment.h |  2 +-
 statinfo.c    | 10 +++++-----
 4 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/entry.c b/entry.c
index 7817aee362..c55e867d8a 100644
--- a/entry.c
+++ b/entry.c
@@ -443,7 +443,8 @@ static int check_path(const char *path, int len, struct stat *st, int skiplen)
 static void mark_colliding_entries(const struct checkout *state,
 				   struct cache_entry *ce, struct stat *st)
 {
-	int trust_ino = check_stat;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
+	int trust_ino = cfg->check_stat;
 
 #if defined(GIT_WINDOWS_NATIVE) || defined(__CYGWIN__)
 	trust_ino = 0;
diff --git a/environment.c b/environment.c
index 0a9067729e..8542ac3141 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
 static int zlib_compression_seen;
 
 int trust_executable_bit = 1;
-int check_stat = 1;
 int has_symlinks = 1;
 int minimum_abbrev = 4, default_abbrev = -1;
 int ignore_case;
@@ -315,9 +314,9 @@ int git_default_core_config(const char *var, const char *value,
 		if (!value)
 			return config_error_nonbool(var);
 		if (!strcasecmp(value, "default"))
-			check_stat = 1;
+			cfg->check_stat = 1;
 		else if (!strcasecmp(value, "minimal"))
-			check_stat = 0;
+			cfg->check_stat = 0;
 		else
 			return error(_("invalid value for '%s': '%s'"),
 				     var, value);
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->apply_sparse_checkout = 0;
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
 	cfg->trust_ctime = 1;
+	cfg->check_stat = 1;
 }
diff --git a/environment.h b/environment.h
index 64d537686e..1d3e2e4f23 100644
--- a/environment.h
+++ b/environment.h
@@ -92,6 +92,7 @@ struct repo_config_values {
 	char *attributes_file;
 	int apply_sparse_checkout;
 	int trust_ctime;
+	int check_stat;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -162,7 +163,6 @@ extern char *git_work_tree_cfg;
 
 /* Environment bits from configuration mechanism */
 extern int trust_executable_bit;
-extern int check_stat;
 extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
 extern int ignore_case;
diff --git a/statinfo.c b/statinfo.c
index 4fc12053f4..5e00af127d 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -68,19 +68,19 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
 
 	if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
 		changed |= MTIME_CHANGED;
-	if (cfg->trust_ctime && check_stat &&
+	if (cfg->trust_ctime && cfg->check_stat &&
 	    sd->sd_ctime.sec != (unsigned int)st->st_ctime)
 		changed |= CTIME_CHANGED;
 
 #ifdef USE_NSEC
-	if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
+	if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
 		changed |= MTIME_CHANGED;
-	if (cfg->trust_ctime && check_stat &&
+	if (cfg->trust_ctime && cfg->check_stat &&
 	    sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
 		changed |= CTIME_CHANGED;
 #endif
 
-	if (check_stat) {
+	if (cfg->check_stat) {
 		if (sd->sd_uid != (unsigned int) st->st_uid ||
 			sd->sd_gid != (unsigned int) st->st_gid)
 			changed |= OWNER_CHANGED;
@@ -94,7 +94,7 @@ int match_stat_data(const struct stat_data *sd, struct stat *st)
 	 * clients will have different views of what "device"
 	 * the filesystem is on
 	 */
-	if (check_stat && sd->sd_dev != (unsigned int) st->st_dev)
+	if (cfg->check_stat && sd->sd_dev != (unsigned int) st->st_dev)
 			changed |= INODE_CHANGED;
 #endif
 
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 1/8] environment: move "trust_ctime" into `struct repo_config_values`
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260601154211.82370-1-belkid98@gmail.com>

The `core.trustctime` configuration is currently stored in the global
variable `trust_ctime`, which makes it shared across repository
instances in a single process.

Store it instead in `repo_config_values`, where eagerly‑parsed
repository configuration lives. `core.trustctime` is parsed eagerly
because it is used in low‑level stat‑matching functions
(`match_stat_data()`), where a lazy parse could cause unexpected
fatal errors, result in a performance regression and complicate
libification efforts. This preserves that behavior while tying the
value to the repository from which it was read, avoiding cross‑repository
state leakage and continuing the effort to reduce reliance on global
configuration state.

Update all references to use repo_config_values().

Mentored-by: Christian Couder <christian.couder@gmail.com>
Signed-off-by: Olamide Caleb Bello <belkid98@gmail.com>
---
 environment.c | 4 ++--
 environment.h | 2 +-
 statinfo.c    | 6 ++++--
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/environment.c b/environment.c
index fc3ed8bb1c..0a9067729e 100644
--- a/environment.c
+++ b/environment.c
@@ -42,7 +42,6 @@ static int pack_compression_seen;
 static int zlib_compression_seen;
 
 int trust_executable_bit = 1;
-int trust_ctime = 1;
 int check_stat = 1;
 int has_symlinks = 1;
 int minimum_abbrev = 4, default_abbrev = -1;
@@ -309,7 +308,7 @@ int git_default_core_config(const char *var, const char *value,
 		return 0;
 	}
 	if (!strcmp(var, "core.trustctime")) {
-		trust_ctime = git_config_bool(var, value);
+		cfg->trust_ctime = git_config_bool(var, value);
 		return 0;
 	}
 	if (!strcmp(var, "core.checkstat")) {
@@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg)
 	cfg->attributes_file = NULL;
 	cfg->apply_sparse_checkout = 0;
 	cfg->branch_track = BRANCH_TRACK_REMOTE;
+	cfg->trust_ctime = 1;
 }
diff --git a/environment.h b/environment.h
index 123a71cdc8..64d537686e 100644
--- a/environment.h
+++ b/environment.h
@@ -91,6 +91,7 @@ struct repo_config_values {
 	/* section "core" config values */
 	char *attributes_file;
 	int apply_sparse_checkout;
+	int trust_ctime;
 
 	/* section "branch" config values */
 	enum branch_track branch_track;
@@ -161,7 +162,6 @@ extern char *git_work_tree_cfg;
 
 /* Environment bits from configuration mechanism */
 extern int trust_executable_bit;
-extern int trust_ctime;
 extern int check_stat;
 extern int has_symlinks;
 extern int minimum_abbrev, default_abbrev;
diff --git a/statinfo.c b/statinfo.c
index 30a164b0e6..4fc12053f4 100644
--- a/statinfo.c
+++ b/statinfo.c
@@ -3,6 +3,7 @@
 #include "git-compat-util.h"
 #include "environment.h"
 #include "statinfo.h"
+#include "repository.h"
 
 /*
  * Munge st_size into an unsigned int.
@@ -63,17 +64,18 @@ void fake_lstat_data(const struct stat_data *sd, struct stat *st)
 int match_stat_data(const struct stat_data *sd, struct stat *st)
 {
 	int changed = 0;
+	struct repo_config_values *cfg = repo_config_values(the_repository);
 
 	if (sd->sd_mtime.sec != (unsigned int)st->st_mtime)
 		changed |= MTIME_CHANGED;
-	if (trust_ctime && check_stat &&
+	if (cfg->trust_ctime && check_stat &&
 	    sd->sd_ctime.sec != (unsigned int)st->st_ctime)
 		changed |= CTIME_CHANGED;
 
 #ifdef USE_NSEC
 	if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st))
 		changed |= MTIME_CHANGED;
-	if (trust_ctime && check_stat &&
+	if (cfg->trust_ctime && check_stat &&
 	    sd->sd_ctime.nsec != ST_CTIME_NSEC(*st))
 		changed |= CTIME_CHANGED;
 #endif
-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply related

* [PATCH v4 0/8] repo_config_values: migrate more globals
From: Olamide Caleb Bello @ 2026-06-01 15:42 UTC (permalink / raw)
  To: git
  Cc: phillip.wood123, christian.couder, usmanakinyemi202,
	kaartic.sivaraam, me, Olamide Caleb Bello
In-Reply-To: <20260423160832.114816-1-belkid98@gmail.com>

Changes since version 3:
- Reword commit subjects for consistency (changed `env` to `environment`)
- Updated commit message in (move "trust_ctime" into `struct repo_config_values`)

Olamide Caleb Bello (8):
  environment: move "trust_ctime" into `struct repo_config_values`
  environment: move "check_stat" into `struct repo_config_values`
  environment: move `zlib_compression_level` into `struct
    repo_config_values`
  environment: move "pack_compression_level" into `struct
    repo_config_values`
  environment: move "precomposed_unicode" into `struct
    repo_config_values`
  environment: move "core_sparse_checkout_cone" into `struct
    repo_config_values`
  environment: move "sparse_expect_files_outside_of_patterns" into
    `repo_config_values`
  environment: move "warn_on_object_refname_ambiguity" into `struct
    repo_config_values`

 builtin/cat-file.c        |  7 ++++---
 builtin/fast-import.c     |  8 +++++---
 builtin/index-pack.c      |  3 ++-
 builtin/mv.c              |  2 +-
 builtin/pack-objects.c    | 24 +++++++++++++----------
 builtin/sparse-checkout.c | 37 +++++++++++++++++++++---------------
 compat/precompose_utf8.c  | 20 +++++++++++++-------
 diff.c                    |  3 ++-
 dir.c                     |  3 ++-
 entry.c                   |  3 ++-
 environment.c             | 40 +++++++++++++++++++++------------------
 environment.h             | 19 ++++++++++---------
 http-push.c               |  3 ++-
 object-file.c             |  6 ++++--
 object-name.c             |  3 ++-
 revision.c                |  7 ++++---
 sparse-index.c            |  4 ++--
 statinfo.c                | 12 +++++++-----
 submodule.c               |  7 ++++---
 upload-pack.c             |  3 ++-
 20 files changed, 126 insertions(+), 88 deletions(-)

-- 
2.53.0.155.g9f36b15afa


^ permalink raw reply

* [PATCH 2/2] builtin/history: implement "drop" subcommand
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
  To: git
In-Reply-To: <20260601-b4-pks-history-drop-v1-0-643e32340d55@pks.im>

A common operation when editing the commit history is to drop a specific
commit from the history entirely, but this operation is not currently
covered by git-history(1).

A couple of noteworthy bits:

  - This is the first git-history(1) command that will ultimately result
    in changes to both the index and the working tree. We thus have to
    add logic to merge resulting changes into those.

  - It is still not possible to replay merge commits, so this limitation
    is inherited for the new "drop" command.

  - For now we refuse to drop root commits. While we _can_ indeed drop
    root commits in the general case, there are edge cases where the
    resulting history would become completely empty. This is thus left
    to a subsequent patch series.

Other than that, most of the logic is rather straight-forward as we can
continue to build on the preexisting logic in git-history(1) for most of
the part.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 Documentation/git-history.adoc |  38 ++-
 builtin/history.c              | 231 +++++++++++++++++++
 t/meson.build                  |   1 +
 t/t3454-history-drop.sh        | 513 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 782 insertions(+), 1 deletion(-)

diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 2ba8121795..4eac732fd2 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
 SYNOPSIS
 --------
 [synopsis]
+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
 git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
 git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
 git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
@@ -51,13 +52,28 @@ be stateful operations. The limitation can be lifted once (if) Git learns about
 first-class conflicts.
 
 When using `fixup` with `--empty=drop`, dropping the root commit is not yet
-supported.
+supported. Likewise, `drop` cannot remove the root commit or a merge commit.
 
 COMMANDS
 --------
 
 The following commands are available to rewrite history in different ways:
 
+`drop <commit>`::
+	Remove the specified commit from the history. All descendants of the
+	commit are replayed directly onto its parent.
++
+The root commit cannot be dropped as that may lead to edge cases where refs
+end up with no commits anymore. Merge commits cannot be dropped either; see
+LIMITATIONS.
++
+If `HEAD` points at a commit that is to be rewritten, the index and working
+tree are updated to match the new `HEAD`. The command aborts before any
+references are updated in case local modifications would be overwritten.
++
+If replaying any descendant would result in a conflict, the command aborts
+with an error.
+
 `fixup <commit>`::
 	Apply the currently staged changes to the specified commit. This is
 	similar in nature to `git commit --fixup=<commit>` followed by `git
@@ -170,6 +186,26 @@ The staged addition of `unrelated.txt` has been incorporated into the `first`
 commit. All descendant commits have been replayed on top of the rewritten
 history.
 
+Drop a commit
+~~~~~~~~~~~~~
+
+----------
+$ git log --oneline
+abc1234 (HEAD -> main) third
+def5678 second
+ghi9012 first
+
+$ git history drop def5678
+
+$ git log --oneline
+jkl3456 (HEAD -> main) third
+ghi9012 first
+----------
+
+The `second` commit has been removed from the history, and `third` has been
+replayed directly on top of `first`. All branches that pointed at the dropped
+commit have been moved to its parent.
+
 Split a commit
 ~~~~~~~~~~~~~~
 
diff --git a/builtin/history.c b/builtin/history.c
index 4fadf38c32..12c27defbb 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -21,9 +21,12 @@
 #include "sequencer.h"
 #include "strvec.h"
 #include "tree.h"
+#include "tree-walk.h"
 #include "unpack-trees.h"
 #include "wt-status.h"
 
+#define GIT_HISTORY_DROP_USAGE \
+	N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
 #define GIT_HISTORY_FIXUP_USAGE \
 	N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
 #define GIT_HISTORY_REWORD_USAGE \
@@ -1001,12 +1004,239 @@ static int cmd_history_split(int argc,
 	return ret;
 }
 
+static int update_worktree(struct repository *repo,
+			   const struct commit *old_head,
+			   const struct commit *new_head,
+			   bool dry_run)
+{
+	struct index_state index = INDEX_STATE_INIT(repo);
+	struct unpack_trees_options opts = { 0 };
+	struct lock_file lock = LOCK_INIT;
+	struct tree_desc desc[2] = { 0 };
+	char *desc_buf[2] = { 0 };
+	int ret;
+
+	if (!dry_run &&
+	    repo_hold_locked_index(repo, &lock, LOCK_REPORT_ON_ERROR) < 0)
+		return -1;
+
+	if (read_index_from(&index, repo->index_file, repo->gitdir) < 0) {
+		ret = error(_("unable to read index"));
+		goto out;
+	}
+
+	setup_unpack_trees_porcelain(&opts, "history drop");
+	opts.head_idx = 1;
+	opts.src_index = &index;
+	opts.dst_index = &index;
+	opts.fn = twoway_merge;
+	opts.merge = 1;
+	opts.update = !dry_run;
+	opts.dry_run = dry_run;
+	opts.preserve_ignored = 0;
+	init_checkout_metadata(&opts.meta, NULL, &new_head->object.oid, NULL);
+
+	desc_buf[0] = fill_tree_descriptor(repo, &desc[0], &old_head->object.oid);
+	desc_buf[1] = fill_tree_descriptor(repo, &desc[1], &new_head->object.oid);
+
+	if (unpack_trees(2, desc, &opts)) {
+		ret = -1;
+		goto out;
+	}
+
+	if (!dry_run) {
+		cache_tree_free(&index.cache_tree);
+
+		if (write_locked_index(&index, &lock, COMMIT_LOCK)) {
+			ret = error(_("could not write index"));
+			goto out;
+		}
+	}
+
+	ret = 0;
+
+out:
+	clear_unpack_trees_porcelain(&opts);
+	rollback_lock_file(&lock);
+	release_index(&index);
+	free(desc_buf[0]);
+	free(desc_buf[1]);
+	return ret;
+}
+
+static int find_head_tree_change(struct repository *repo,
+				 const struct replay_result *result,
+				 struct commit **old_head,
+				 struct commit **new_head,
+				 bool *changed)
+{
+	const struct replay_ref_update *head_update = NULL;
+	struct commit *old_head_commit, *new_head_commit;
+	struct tree *old_head_tree, *new_head_tree;
+	const char *head_target;
+	int head_flags;
+
+	*changed = false;
+
+	head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo),
+					      "HEAD", RESOLVE_REF_NO_RECURSE,
+					      NULL, &head_flags);
+	if (!head_target)
+		return error(_("cannot look up HEAD"));
+	if (!(head_flags & REF_ISSYMREF))
+		head_target = "HEAD";
+
+	for (size_t i = 0; i < result->updates_nr; i++) {
+		if (!strcmp(result->updates[i].refname, head_target)) {
+			head_update = &result->updates[i];
+			break;
+		}
+	}
+
+	if (!head_update)
+		return 0;
+
+	old_head_commit = lookup_commit_reference(repo, &head_update->old_oid);
+	new_head_commit = lookup_commit_reference(repo, &head_update->new_oid);
+	if (!old_head_commit || !new_head_commit)
+		return error(_("cannot resolve HEAD commit"));
+
+	old_head_tree = repo_get_commit_tree(repo, old_head_commit);
+	new_head_tree = repo_get_commit_tree(repo, new_head_commit);
+	if (!old_head_tree || !new_head_tree)
+		return error(_("cannot resolve tree for HEAD"));
+
+	if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid))
+		return 0;
+
+	*old_head = old_head_commit;
+	*new_head = new_head_commit;
+	*changed = true;
+
+	return 0;
+}
+
+static int cmd_history_drop(int argc,
+			    const char **argv,
+			    const char *prefix,
+			    struct repository *repo)
+{
+	const char * const usage[] = {
+		GIT_HISTORY_DROP_USAGE,
+		NULL,
+	};
+	enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+	enum ref_action action = REF_ACTION_DEFAULT;
+	int dry_run = 0;
+	struct option options[] = {
+		OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+			       N_("control which refs should be updated"),
+			       PARSE_OPT_NONEG, parse_ref_action),
+		OPT_BOOL('n', "dry-run", &dry_run,
+			 N_("perform a dry-run without updating any refs")),
+		OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+			       N_("how to handle descendants that become empty"),
+			       PARSE_OPT_NONEG, parse_opt_empty),
+		OPT_END(),
+	};
+	struct strbuf reflog_msg = STRBUF_INIT;
+	struct commit *original, *rewritten;
+	struct rev_info revs = { 0 };
+	struct replay_result result = { 0 };
+	struct commit *old_head, *new_head;
+	bool head_moves = false;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, options, usage, 0);
+	if (argc != 1) {
+		ret = error(_("command expects a single revision"));
+		goto out;
+	}
+	repo_config(repo, git_default_config, NULL);
+
+	if (action == REF_ACTION_DEFAULT)
+		action = REF_ACTION_BRANCHES;
+
+	original = lookup_commit_reference_by_name(argv[0]);
+	if (!original) {
+		ret = error(_("commit cannot be found: %s"), argv[0]);
+		goto out;
+	}
+
+	if (!original->parents) {
+		ret = error(_("cannot drop root commit %s: "
+			      "it has no parent to replay onto"),
+			    argv[0]);
+		goto out;
+	} else if (original->parents->next) {
+		ret = error(_("cannot drop merge commit"));
+		goto out;
+	}
+
+	ret = setup_revwalk(repo, action, original, &revs);
+	if (ret)
+		goto out;
+
+	rewritten = original->parents->item;
+
+	ret = compute_pending_ref_updates(&revs, action, original, rewritten,
+					  empty, &result);
+	if (ret) {
+		ret = error(_("failed replaying descendants"));
+		goto out;
+	}
+
+	/*
+	 * If HEAD will move as a result of the rewrite then we'll have to
+	 * merge in the changes into the worktree and index. This merge can of
+	 * course conflict, which will cause the whole operation to abort.
+	 *
+	 * If we had already updated the refs at that point then we'd have an
+	 * inconsistent repository state. So we first perform a dry-run merge
+	 * here before updating refs.
+	 */
+	if (!dry_run && !is_bare_repository()) {
+		ret = find_head_tree_change(repo, &result, &old_head,
+					    &new_head, &head_moves);
+		if (ret < 0)
+			goto out;
+
+		if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) {
+			ret = error(_("dropping this commit would "
+				      "overwrite local changes; aborting"));
+			goto out;
+		}
+	}
+
+	strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]);
+	ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run);
+	if (ret < 0) {
+		ret = error(_("failed to update references"));
+		goto out;
+	}
+
+	if (head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
+		ret = error(_("failed to update working tree; "
+			      "run `git checkout HEAD` to sync"));
+		goto out;
+	}
+
+	ret = 0;
+
+out:
+	replay_result_release(&result);
+	strbuf_release(&reflog_msg);
+	release_revisions(&revs);
+	return ret;
+}
+
 int cmd_history(int argc,
 		const char **argv,
 		const char *prefix,
 		struct repository *repo)
 {
 	const char * const usage[] = {
+		GIT_HISTORY_DROP_USAGE,
 		GIT_HISTORY_FIXUP_USAGE,
 		GIT_HISTORY_REWORD_USAGE,
 		GIT_HISTORY_SPLIT_USAGE,
@@ -1014,6 +1244,7 @@ int cmd_history(int argc,
 	};
 	parse_opt_subcommand_fn *fn = NULL;
 	struct option options[] = {
+		OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
 		OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
 		OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
 		OPT_SUBCOMMAND("split", &fn, cmd_history_split),
diff --git a/t/meson.build b/t/meson.build
index 2af8d01279..d5e71056b2 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -399,6 +399,7 @@ integration_tests = [
   't3451-history-reword.sh',
   't3452-history-split.sh',
   't3453-history-fixup.sh',
+  't3454-history-drop.sh',
   't3500-cherry.sh',
   't3501-revert-cherry-pick.sh',
   't3502-cherry-pick-merge.sh',
diff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh
new file mode 100755
index 0000000000..b320ff09b3
--- /dev/null
+++ b/t/t3454-history-drop.sh
@@ -0,0 +1,513 @@
+#!/bin/sh
+
+test_description='tests for git-history drop subcommand'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-log-graph.sh"
+
+expect_graph () {
+	cat >expect &&
+	lib_test_cmp_graph --graph --format=%s "$@"
+}
+
+expect_log () {
+	git log --format="%s" "$@" >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+test_expect_success 'errors on missing commit argument' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on too many arguments' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop HEAD HEAD 2>err &&
+		test_grep "command expects a single revision" err
+	)
+'
+
+test_expect_success 'errors on unknown revision' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		test_must_fail git history drop does-not-exist 2>err &&
+		test_grep "commit cannot be found: does-not-exist" err
+	)
+'
+
+test_expect_success 'errors with invalid --empty= value' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	test_commit -C repo initial &&
+	test_commit -C repo second &&
+	test_must_fail git -C repo history drop --empty=bogus HEAD 2>err &&
+	test_grep "unrecognized.*--empty.*bogus" err
+'
+
+test_expect_success 'drops a commit in the middle and replays descendants' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git symbolic-ref HEAD >expect &&
+		git history drop HEAD~ &&
+		git symbolic-ref HEAD >actual &&
+		test_cmp expect actual &&
+
+		expect_log <<-\EOF &&
+		third
+		first
+		EOF
+
+		test_must_fail git show HEAD:second.t &&
+		test_path_is_missing second.t &&
+
+		git reflog >reflog &&
+		test_grep "drop: dropping HEAD~" reflog
+	)
+'
+
+test_expect_success 'drops the HEAD commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		git history drop HEAD &&
+
+		expect_log <<-\EOF
+		first
+		EOF
+	)
+'
+
+test_expect_success 'drops a commit on detached HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+		git checkout --detach HEAD &&
+
+		git history drop HEAD~ &&
+
+		expect_log <<-\EOF
+		third
+		first
+		EOF
+	)
+'
+
+# Note: in this case it would actually be fine to drop the root commit, as we
+# do have a descendant commit, and no reference points to the root commit
+# directly. So this is something that we may relax eventually.
+test_expect_success 'refuses to drop the root commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		test_must_fail git history drop HEAD~ 2>err &&
+		test_grep "cannot drop root commit" err
+	)
+'
+
+# In contrast to the above case, we actually don't want to drop the root commit
+# here as that would cause us to end up with an empty commit graph.
+test_expect_success 'refuses to drop the root commit when branch becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "cannot drop root commit" err
+	)
+'
+
+test_expect_success 'refuses to drop a merge commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+		git switch - &&
+		git merge theirs &&
+
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "cannot drop merge commit" err
+	)
+'
+
+test_expect_success 'refuses when descendants contain a merge commit' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit middle &&
+		git branch branch &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+		git switch - &&
+		git merge theirs &&
+
+		test_must_fail git history drop middle 2>err &&
+		test_grep "replaying merge commits is not supported yet" err
+	)
+'
+
+test_expect_success 'works in a bare repository' '
+	test_when_finished "rm -rf repo repo.git" &&
+
+	git init repo &&
+	test_commit -C repo first &&
+	test_commit -C repo second &&
+	test_commit -C repo third &&
+
+	git clone --bare repo repo.git &&
+	(
+		cd repo.git &&
+
+		git history drop HEAD~ &&
+		expect_log <<-\EOF
+		third
+		first
+		EOF
+	)
+'
+
+test_expect_success 'updates branches on other lines of descent' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit target &&
+		git branch theirs &&
+		test_commit ours &&
+		git switch theirs &&
+		test_commit theirs &&
+
+		expect_graph --branches <<-\EOF &&
+		* theirs
+		| * ours
+		|/
+		* target
+		* base
+		EOF
+
+		git history drop target &&
+
+		expect_graph --branches <<-\EOF
+		* ours
+		| * theirs
+		|/
+		* base
+		EOF
+	)
+'
+
+test_expect_success 'moves branch pointing at dropped commit to its parent' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		git branch points-at-second &&
+		test_commit third &&
+
+		git rev-parse first >expect &&
+		git history drop second &&
+		git rev-parse points-at-second >actual &&
+		test_cmp expect actual &&
+
+		expect_log --format="%s %D" --branches <<-\EOF
+		third HEAD -> main
+		first tag: first, points-at-second
+		EOF
+	)
+'
+
+test_expect_success '--dry-run prints ref updates without modifying repo' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		git branch branch &&
+		test_commit middle &&
+		test_commit ours &&
+		git switch branch &&
+		test_commit theirs &&
+
+		git refs list >refs-expect &&
+		git history drop --dry-run main~ >updates &&
+		git refs list >refs-actual &&
+		test_cmp refs-expect refs-actual &&
+		test_grep "update refs/heads/main" updates &&
+
+		git update-ref --stdin <updates &&
+		expect_log main <<-\EOF
+		ours
+		base
+		EOF
+	)
+'
+
+test_expect_success '--update-refs=head updates only HEAD' '
+	test_when_finished "rm -rf repo" &&
+	git init repo --initial-branch=main &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit target &&
+		git branch theirs &&
+		test_commit ours &&
+		git switch theirs &&
+		test_commit theirs &&
+
+		# When told to update HEAD only, the command refuses to
+		# rewrite commits that are not an ancestor of HEAD.
+		test_must_fail git history drop --update-refs=head main 2>err &&
+		test_grep "rewritten commit must be an ancestor of HEAD" err &&
+
+		expect_graph --branches <<-\EOF &&
+		* theirs
+		| * ours
+		|/
+		* target
+		* base
+		EOF
+
+		git switch main &&
+		git history drop --update-refs=head target &&
+
+		expect_graph --branches <<-\EOF
+		* ours
+		| * theirs
+		| * target
+		|/
+		* base
+		EOF
+	)
+'
+
+test_expect_success 'conflict with replayed commit aborts cleanly' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit conflict-a file &&
+		test_commit conflict-b file &&
+
+		git refs list >refs-expect &&
+		test_must_fail git history drop HEAD~ 2>err &&
+		test_grep "failed replaying descendants" err &&
+		git refs list >refs-actual &&
+		test_cmp refs-expect refs-actual
+	)
+'
+
+# Build a history where a descendant of the drop target reverts the change
+# introduced by the drop target. After dropping, the descendant's diff applies
+# against a tree that already lacks the change, so it becomes empty.
+setup_empty_descendant_repo () {
+	git init "$1" &&
+	(
+		cd "$1" &&
+		echo C1 >file &&
+		git add file &&
+		git commit -m "base" &&
+		git tag base &&
+		echo C2 >file &&
+		git add file &&
+		git commit -m "drop-me" &&
+		git tag drop-me &&
+		test_commit middle &&
+		echo C1 >file &&
+		git add file &&
+		git commit -m "revert-drop-me" &&
+		git tag revert-drop-me
+	)
+}
+
+test_expect_success '--empty=drop drops descendants that become empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		git history drop --empty=drop drop-me &&
+
+		expect_log <<-\EOF
+		middle
+		base
+		EOF
+	)
+'
+
+test_expect_success '--empty=keep keeps descendants that become empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		git history drop --empty=keep drop-me &&
+
+		expect_log <<-\EOF &&
+		revert-drop-me
+		middle
+		base
+		EOF
+		git diff HEAD~ HEAD >diff &&
+		test_must_be_empty diff
+	)
+'
+
+test_expect_success '--empty=abort errors out when a descendant becomes empty' '
+	test_when_finished "rm -rf repo" &&
+	setup_empty_descendant_repo repo &&
+	(
+		cd repo &&
+
+		test_must_fail git history drop --empty=abort drop-me 2>err &&
+		test_grep "became empty after replay" err
+	)
+'
+
+test_expect_success 'updates index and worktree when HEAD moves' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+		test_commit third &&
+
+		git history drop second &&
+
+		# Worktree should no longer contain second.t.
+		test_path_is_missing second.t &&
+		test_path_is_file first.t &&
+		test_path_is_file third.t &&
+
+		# Index and worktree should both match the new HEAD.
+		git status --porcelain --untracked-files=no >status &&
+		test_must_be_empty status
+	)
+'
+
+test_expect_success 'updates worktree when dropping HEAD itself' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		test_commit second &&
+
+		git history drop HEAD &&
+
+		test_path_is_missing second.t &&
+		test_path_is_file first.t &&
+
+		git status --porcelain --untracked-files=no >status &&
+		test_must_be_empty status
+	)
+'
+
+test_expect_success 'preserves unrelated unstaged modifications' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		echo first-content >unrelated.txt &&
+		git add unrelated.txt &&
+		git commit -m "add unrelated" &&
+		test_commit second &&
+		test_commit third &&
+
+		echo locally-modified >unrelated.txt &&
+
+		git diff >diff-expect &&
+		git history drop second &&
+		git diff >diff-actual &&
+		test_cmp diff-expect diff-actual &&
+		test_path_is_missing second.t
+	)
+'
+
+test_expect_success 'preserves unrelated staged changes' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit first &&
+		echo first-content >unrelated.txt &&
+		git add unrelated.txt &&
+		git commit -m "add unrelated" &&
+		test_commit second &&
+		test_commit third &&
+
+		echo staged-change >unrelated.txt &&
+		git add unrelated.txt &&
+
+		git diff --cached >diff-expect &&
+		git history drop second &&
+		git diff --cached >diff-actual &&
+		test_cmp diff-expect diff-actual &&
+		test_path_is_missing second.t
+	)
+'
+
+test_expect_success 'aborts when local modifications would be overwritten' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit base &&
+		test_commit conflict &&
+
+		echo local-edit >conflict.t &&
+		git diff >diff-expect &&
+		test_must_fail git history drop HEAD 2>err &&
+		test_grep "would overwrite local changes" err &&
+		git diff >diff-actual &&
+		test_cmp diff-expect diff-actual
+	)
+'
+
+test_done

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH 1/2] builtin/history: split handling of ref updates into two phases
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
  To: git
In-Reply-To: <20260601-b4-pks-history-drop-v1-0-643e32340d55@pks.im>

The function `handle_reference_updates()` is used by git-history(1) to
update all references that refer to commits that have been rewritten. As
such, it performs two steps:

  - It gathers the references that need to be updated in the first
    place.

  - It prepares and commits the reference transaction.

In a subsequent commit we'll want to handle those two steps separately.
Prepare for this by splitting up the function into two.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 builtin/history.c | 102 ++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 64 insertions(+), 38 deletions(-)

diff --git a/builtin/history.c b/builtin/history.c
index 0fc06fb204..4fadf38c32 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -333,21 +333,17 @@ static int handle_ref_update(struct ref_transaction *transaction,
 				      NULL, NULL, 0, reflog_msg, err);
 }
 
-static int handle_reference_updates(struct rev_info *revs,
-				    enum ref_action action,
-				    struct commit *original,
-				    struct commit *rewritten,
-				    const char *reflog_msg,
-				    int dry_run,
-				    enum replay_empty_commit_action empty)
+static int compute_pending_ref_updates(struct rev_info *revs,
+				       enum ref_action action,
+				       struct commit *original,
+				       struct commit *rewritten,
+				       enum replay_empty_commit_action empty,
+				       struct replay_result *result)
 {
 	const struct name_decoration *decoration;
 	struct replay_revisions_options opts = {
 		.empty = empty,
 	};
-	struct replay_result result = { 0 };
-	struct ref_transaction *transaction = NULL;
-	struct strbuf err = STRBUF_INIT;
 	char hex[GIT_MAX_HEXSZ + 1];
 	bool detached_head;
 	int head_flags = 0;
@@ -359,34 +355,13 @@ static int handle_reference_updates(struct rev_info *revs,
 
 	opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
 
-	ret = replay_revisions(revs, &opts, &result);
+	ret = replay_revisions(revs, &opts, result);
 	if (ret)
-		goto out;
+		return ret;
 
 	if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
 		BUG("unsupported ref action %d", action);
 
-	if (!dry_run) {
-		transaction = ref_store_transaction_begin(get_main_ref_store(revs->repo), 0, &err);
-		if (!transaction) {
-			ret = error(_("failed to begin ref transaction: %s"), err.buf);
-			goto out;
-		}
-	}
-
-	for (size_t i = 0; i < result.updates_nr; i++) {
-		ret = handle_ref_update(transaction,
-					result.updates[i].refname,
-					&result.updates[i].new_oid,
-					&result.updates[i].old_oid,
-					reflog_msg, &err);
-		if (ret) {
-			ret = error(_("failed to update ref '%s': %s"),
-				    result.updates[i].refname, err.buf);
-			goto out;
-		}
-	}
-
 	/*
 	 * `replay_revisions()` only updates references that are
 	 * ancestors of `rewritten`, so we need to manually
@@ -414,14 +389,43 @@ static int handle_reference_updates(struct rev_info *revs,
 		    !detached_head)
 			continue;
 
+		ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
+		result->updates[result->updates_nr].refname = xstrdup(decoration->name);
+		result->updates[result->updates_nr].old_oid = original->object.oid;
+		result->updates[result->updates_nr].new_oid = rewritten->object.oid;
+		result->updates_nr++;
+	}
+
+	return 0;
+}
+
+static int apply_pending_ref_updates(struct repository *repo,
+				     const struct replay_result *result,
+				     const char *reflog_msg,
+				     int dry_run)
+{
+	struct ref_transaction *transaction = NULL;
+	struct strbuf err = STRBUF_INIT;
+	int ret;
+
+	if (!dry_run) {
+		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
+							  0, &err);
+		if (!transaction) {
+			ret = error(_("failed to begin ref transaction: %s"), err.buf);
+			goto out;
+		}
+	}
+
+	for (size_t i = 0; i < result->updates_nr; i++) {
 		ret = handle_ref_update(transaction,
-					decoration->name,
-					&rewritten->object.oid,
-					&original->object.oid,
+					result->updates[i].refname,
+					&result->updates[i].new_oid,
+					&result->updates[i].old_oid,
 					reflog_msg, &err);
 		if (ret) {
 			ret = error(_("failed to update ref '%s': %s"),
-				    decoration->name, err.buf);
+				    result->updates[i].refname, err.buf);
 			goto out;
 		}
 	}
@@ -435,11 +439,33 @@ static int handle_reference_updates(struct rev_info *revs,
 
 out:
 	ref_transaction_free(transaction);
-	replay_result_release(&result);
 	strbuf_release(&err);
 	return ret;
 }
 
+static int handle_reference_updates(struct rev_info *revs,
+				    enum ref_action action,
+				    struct commit *original,
+				    struct commit *rewritten,
+				    const char *reflog_msg,
+				    int dry_run,
+				    enum replay_empty_commit_action empty)
+{
+	struct replay_result result = { 0 };
+	int ret;
+
+	ret = compute_pending_ref_updates(revs, action, original, rewritten,
+					  empty, &result);
+	if (ret)
+		goto out;
+
+	ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run);
+
+out:
+	replay_result_release(&result);
+	return ret;
+}
+
 static int commit_became_empty(struct repository *repo,
 			       struct commit *original,
 			       struct tree *result)

-- 
2.54.0.926.g75ba10bac6.dirty


^ permalink raw reply related

* [PATCH 0/2] builtin/history: introduce "drop" subcommand
From: Patrick Steinhardt @ 2026-06-01 15:36 UTC (permalink / raw)
  To: git

Hi,

this small patch series introduces the new "drop" subcommand for
git-history(1). As a reader might guess, the command does exactly that:
given a commit, it will drop that commit from the commit history and
replay descendant branches on top of it.

Thanks!

Patrick

---
Patrick Steinhardt (2):
      builtin/history: split handling of ref updates into two phases
      builtin/history: implement "drop" subcommand

 Documentation/git-history.adoc |  38 ++-
 builtin/history.c              | 333 +++++++++++++++++++++++---
 t/meson.build                  |   1 +
 t/t3454-history-drop.sh        | 513 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 846 insertions(+), 39 deletions(-)


---
base-commit: 1666c1265231b0bc5f613fbbf3f0a9896cdef76e
change-id: 20260601-b4-pks-history-drop-28f6c6399e7b


^ permalink raw reply


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