* Re: [PATCH] wt-status: avoid quadratic insertion for untracked paths
From: Sahitya Chandra @ 2026-07-17 14:40 UTC (permalink / raw)
To: Jeff King; +Cc: Patrick Steinhardt, git, gitster, avarab, stolee
In-Reply-To: <20260717075449.GA1832790@coredump.intra.peff.net>
On Fri, Jul 17, 2026 at 1:24 PM Jeff King <peff@peff.net> wrote:
> Yeah, I had the same question, and tried for a moment to produce an
> example before realizing that it probably is theoretical. If we are
> feeding the entries in pre-sorted order then the insert is always O(1).
>
> I think it's still worth doing this, though, as it makes the result much
> more obvious to analyze. I think it could even be O(n) if the sort
> implementation is optimized under the hood for pre-sorted inputs.
Thanks, that makes sense. I updated v2 to avoid claiming this is a
current O(n^2) problem and instead frame it as making the append, sort,
and deduplicate steps explicit.
I also switched to string_list_sort_u() as Patrick suggested.
^ permalink raw reply
* Re: [PATCH] wt-status: avoid quadratic insertion for untracked paths
From: Sahitya Chandra @ 2026-07-17 14:37 UTC (permalink / raw)
To: ps; +Cc: git, gitster, avarab, stolee, peff
In-Reply-To: <alnLPSnOt_Sf7cA5@pks.im>
On Fri, Jul 17, 2026 at 11:57 AM Patrick Steinhardt <ps@pks.im> wrote:
> Out of curiosity: is this something that you have encountered in the
> real world as inefficient, or is this rather a theoretical inefficiency?
> If the former it would be great to add a small benchmark to the commit
> message.
Thanks for asking. I do not have a real-world benchmark for this. After
Jeff's reply, I agree that the O(n^2) claim is too strong for the current
code path because fill_directory() already returns the entries sorted, so
string_list_insert() should usually append at the end.
I have reworded v2 to avoid that performance claim and describe the
change as making the collection strategy explicit instead.
> Instead of sorting and then deduplicating you can call
> `string_list_sort_u()`. It does the exact same thing as you do here, but
> I guess it makes sense to use that interface anyway.
Done in v2. Thanks.
^ permalink raw reply
* [PATCH] userdiff: add support for Swift
From: Shlok Kulshreshtha @ 2026-07-17 14:02 UTC (permalink / raw)
To: git
Cc: Shlok Kulshreshtha, D. Ben Knoble, Junio C Hamano,
René Scharfe, Johannes Sixt, Eric Sunshine, Scott L. Burson
Add a built-in userdiff driver for the Swift programming language so that
diff hunk headers and word diffs work out of the box for ".swift" files.
The funcname pattern is built for Swift's own declaration grammar: an
optional run of attributes ("@objc", "@available(iOS 13, *)", ...),
followed by an optional run of lowercase modifiers ("public", "static",
"final", ...), followed by a declaration keyword (func, class, struct,
enum, protocol, extension, actor, init, deinit, subscript). The keyword
is followed by a boundary that allows whitespace, "(" (init/subscript),
"?" or "!" (failable init), or "<" (generics), while still acting as a
word boundary so e.g. "initialize(" does not match.
The word regex recognizes Swift identifiers, hexadecimal, octal, binary,
integer and floating-point literals, and the language's operators.
Signed-off-by: Shlok Kulshreshtha <diy2903@gmail.com>
---
This addresses the "add a userdiff driver for a language" microproject.
Swift is not covered by a built-in driver yet, and I did not find an
in-flight patch adding one on the list; please let me know if one exists.
Motivation: without a Swift driver, ".swift" files use the generic
funcname heuristic, so "git diff" hunk headers name the enclosing type
instead of the changed function.
The pattern is built directly from Swift's declaration grammar rather
than adapted from another language's driver, so it covers a few things
that a straight port would miss:
- attributes, with or without arguments, whether on their own line
("@objc" above a "func") or inline with the declaration
("@objc func foo()", "@available(iOS 13, *) public func bar()");
- modifiers ("public", "static", "override", "mutating", ...) in any
combination, before or after attributes;
- failable initializers, "init?" and "init!";
- generics, "init<T>" and "subscript<T>";
- the keyword boundary still acts as a word boundary, so e.g.
"initialize(" is not mistaken for "init".
I verified all of the above against a built binary, including generic
functions with "where" clauses, multi-line signatures, and operator
functions such as "static func ==". I did not find a real case this
pattern misses; the ones I intentionally left out are one-line
declarations ("typealias", "associatedtype") and rarer forms
("operator", "precedencegroup", "macro"), which would not make useful
section headers anyway.
The word regex covers Swift identifiers; hexadecimal, octal, binary,
integer and floating-point literals; and operators including "<<=" /
">>=", "??" and the range operators. All of t4018 passes, including the
sorted builtin_drivers check and the new swift-* fixtures.
Documentation/gitattributes.adoc | 2 ++
t/t4018/swift-actor | 5 +++++
t/t4018/swift-attribute-with-args | 7 +++++++
t/t4018/swift-class | 5 +++++
t/t4018/swift-enum | 5 +++++
t/t4018/swift-extension | 5 +++++
t/t4018/swift-failable-init | 7 +++++++
t/t4018/swift-func | 5 +++++
t/t4018/swift-generic-subscript | 7 +++++++
t/t4018/swift-init | 7 +++++++
t/t4018/swift-inline-attribute | 7 +++++++
t/t4018/swift-modifiers | 4 ++++
t/t4018/swift-protocol | 5 +++++
t/t4018/swift-struct | 5 +++++
userdiff.c | 10 ++++++++++
15 files changed, 86 insertions(+)
create mode 100644 t/t4018/swift-actor
create mode 100644 t/t4018/swift-attribute-with-args
create mode 100644 t/t4018/swift-class
create mode 100644 t/t4018/swift-enum
create mode 100644 t/t4018/swift-extension
create mode 100644 t/t4018/swift-failable-init
create mode 100644 t/t4018/swift-func
create mode 100644 t/t4018/swift-generic-subscript
create mode 100644 t/t4018/swift-init
create mode 100644 t/t4018/swift-inline-attribute
create mode 100644 t/t4018/swift-modifiers
create mode 100644 t/t4018/swift-protocol
create mode 100644 t/t4018/swift-struct
diff --git a/Documentation/gitattributes.adoc b/Documentation/gitattributes.adoc
index bd76167a45..9fea75f96f 100644
--- a/Documentation/gitattributes.adoc
+++ b/Documentation/gitattributes.adoc
@@ -914,6 +914,8 @@ patterns are available:
- `scheme` suitable for source code in most Lisp dialects,
including Scheme, Emacs Lisp, Common Lisp, and Clojure.
+- `swift` suitable for source code in the Swift language.
+
- `tex` suitable for source code for LaTeX documents.
diff --git a/t/t4018/swift-actor b/t/t4018/swift-actor
new file mode 100644
index 0000000000..e4852f40a7
--- /dev/null
+++ b/t/t4018/swift-actor
@@ -0,0 +1,5 @@
+actor RIGHT {
+ let a = 1
+ // a comment
+ let b = ChangeMe
+}
diff --git a/t/t4018/swift-attribute-with-args b/t/t4018/swift-attribute-with-args
new file mode 100644
index 0000000000..22b1ee32f1
--- /dev/null
+++ b/t/t4018/swift-attribute-with-args
@@ -0,0 +1,7 @@
+struct View {
+ @available(iOS 13, *) public func RIGHT() {
+ let a = 1
+ // a comment
+ print(ChangeMe)
+ }
+}
diff --git a/t/t4018/swift-class b/t/t4018/swift-class
new file mode 100644
index 0000000000..c3a9336027
--- /dev/null
+++ b/t/t4018/swift-class
@@ -0,0 +1,5 @@
+class RIGHT {
+ let a = 1
+ // a comment
+ let b = ChangeMe
+}
diff --git a/t/t4018/swift-enum b/t/t4018/swift-enum
new file mode 100644
index 0000000000..0a84302993
--- /dev/null
+++ b/t/t4018/swift-enum
@@ -0,0 +1,5 @@
+enum RIGHT {
+ case first
+ // a comment
+ case ChangeMe
+}
diff --git a/t/t4018/swift-extension b/t/t4018/swift-extension
new file mode 100644
index 0000000000..cbc18ab6ef
--- /dev/null
+++ b/t/t4018/swift-extension
@@ -0,0 +1,5 @@
+extension RIGHT {
+ static let a = 1
+ // a comment
+ static let b = ChangeMe
+}
diff --git a/t/t4018/swift-failable-init b/t/t4018/swift-failable-init
new file mode 100644
index 0000000000..5e4091d97c
--- /dev/null
+++ b/t/t4018/swift-failable-init
@@ -0,0 +1,7 @@
+class Bar {
+ init?(RIGHT: Int) {
+ let value = RIGHT
+ // a comment
+ print(ChangeMe)
+ }
+}
diff --git a/t/t4018/swift-func b/t/t4018/swift-func
new file mode 100644
index 0000000000..1fecae0911
--- /dev/null
+++ b/t/t4018/swift-func
@@ -0,0 +1,5 @@
+func RIGHT(x: Int) -> Int {
+ let y = x
+ // a comment
+ return ChangeMe
+}
diff --git a/t/t4018/swift-generic-subscript b/t/t4018/swift-generic-subscript
new file mode 100644
index 0000000000..565f93cd6c
--- /dev/null
+++ b/t/t4018/swift-generic-subscript
@@ -0,0 +1,7 @@
+struct Container {
+ subscript<RIGHT>(index: RIGHT) -> Int {
+ let a = 0
+ // a comment
+ return ChangeMe
+ }
+}
diff --git a/t/t4018/swift-init b/t/t4018/swift-init
new file mode 100644
index 0000000000..f683e74794
--- /dev/null
+++ b/t/t4018/swift-init
@@ -0,0 +1,7 @@
+class Foo {
+ init(RIGHT: Int) {
+ let value = RIGHT
+ // a comment
+ print(ChangeMe)
+ }
+}
diff --git a/t/t4018/swift-inline-attribute b/t/t4018/swift-inline-attribute
new file mode 100644
index 0000000000..2374c4b603
--- /dev/null
+++ b/t/t4018/swift-inline-attribute
@@ -0,0 +1,7 @@
+class Service {
+ @objc func RIGHT() {
+ let path = "/api"
+ // a comment
+ log(ChangeMe)
+ }
+}
diff --git a/t/t4018/swift-modifiers b/t/t4018/swift-modifiers
new file mode 100644
index 0000000000..9d80685a78
--- /dev/null
+++ b/t/t4018/swift-modifiers
@@ -0,0 +1,4 @@
+public static func RIGHT() -> Int {
+ // a comment
+ return ChangeMe
+}
diff --git a/t/t4018/swift-protocol b/t/t4018/swift-protocol
new file mode 100644
index 0000000000..07c39ec2a3
--- /dev/null
+++ b/t/t4018/swift-protocol
@@ -0,0 +1,5 @@
+protocol RIGHT {
+ var first: Int { get }
+ // a comment
+ var second: ChangeMe { get }
+}
diff --git a/t/t4018/swift-struct b/t/t4018/swift-struct
new file mode 100644
index 0000000000..e399ed7759
--- /dev/null
+++ b/t/t4018/swift-struct
@@ -0,0 +1,5 @@
+struct RIGHT {
+ let a = 1
+ // a comment
+ let b = ChangeMe
+}
diff --git a/userdiff.c b/userdiff.c
index b5412e6bc3..df37dd78a6 100644
--- a/userdiff.c
+++ b/userdiff.c
@@ -362,6 +362,16 @@ PATTERNS("scheme",
"\\|([^|\\\\]|\\\\.)*\\|"
/* All other words should be delimited by spaces or parentheses. */
"|([^][)(}{ \t])+"),
+PATTERNS("swift",
+ "^[ \t]*((@[A-Za-z_][A-Za-z0-9_]*(\\([^()]*\\))?[ \t]+)*([a-z]+[ \t]+)*(func|init|deinit|subscript|class|struct|enum|protocol|extension|actor)[ \t(?!<].*)$",
+ /* -- */
+ "[a-zA-Z_][a-zA-Z0-9_]*"
+ /* hexadecimal, octal, and binary literals */
+ "|0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+"
+ /* integers and floating-point numbers */
+ "|[0-9][0-9_]*([.][0-9_]+)?([eE][-+]?[0-9]+)?"
+ /* unary and binary operators */
+ "|[-+*/%<>=!&|^~?]=?|&&|\\|\\||<<=?|>>=?|\\?\\?|\\.\\.[.<]|->"),
PATTERNS("tex", "^(\\\\((sub)*section|chapter|part)\\*{0,1}\\{.*)$",
"\\\\[a-zA-Z@]+|\\\\.|([a-zA-Z0-9]|[^\x01-\x7f])+"),
{ .name = "default", .binary = -1 },
--
2.52.0
^ permalink raw reply related
* [PATCH v2] gitweb: shorten index hashes with trailing file modes
From: Travor Liu @ 2026-07-17 13:52 UTC (permalink / raw)
To: git; +Cc: gitster, Travor Liu
In-Reply-To: <SA1PR10MB997715AD62D7F2AF64EB1A9887F1F82@SA1PR10MB997715.namprd10.prod.outlook.com>
From: Travor Liu <travor_lzh@outlook.com>
Diff index lines have included a trailing file mode since ec1fcc16af
(Show original and resulting blob object info in diff output,
2005-10-07) when the old and new file modes match:
index <old>..<new> 100644
gitweb recognizes that trailing mode before it tries to shorten and
link the object IDs. This appends the file-type annotation first, but
the object-ID matcher requires the ID range to end the line. As a
result, this common form keeps both full object IDs as plain text.
That is inconsistent with other hash displays and makes commitdiff
output wider than necessary. Recent gitweb changes have fixed mobile
overflow in log, commit, blob and diff views; leaving two full object
IDs in this header preserves an avoidable long line in the diff header.
* gitweb/gitweb.perl: Remove the trailing mode before matching the index
IDs, then append it again after the IDs have been shortened and linked.
This preserves the mode display while letting ordinary and combined
index lines use the existing object-ID formatting paths.
* t/t9502-gitweb-standalone-parse-output.sh: Add coverage for that
common form by rendering a commitdiff for a regular file modification.
Check that the visible index line contains linked short blob IDs
followed by the mode and file-type annotation, and that the full
unlinked form is not emitted.
Signed-off-by: Travor Liu <travor_lzh@outlook.com>
---
Changes since v1:
- Squashed the regression test into the implementation patch.
- Replaced raw grep invocations with test_grep.
gitweb/gitweb.perl | 18 +++++++++++++-----
t/t9502-gitweb-standalone-parse-output.sh | 13 +++++++++++++
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index fde8045..8c2d9b8 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -2339,12 +2339,14 @@ sub format_extended_diff_header_line {
$line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
esc_path($to->{'file'}));
}
- # match single <mode>
- if ($line =~ m/\s(\d{6})$/) {
- $line .= '<span class="info"> (' .
- file_type_long($1) .
- ')</span>';
+
+ # Temporarily remove a trailing <mode> so an index line ends with its
+ # object IDs and can be shortened below.
+ my $mode;
+ if ($line =~ s/\s(\d{6})$//) {
+ $mode = $1;
}
+
# match <hash>
if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") |
$line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) {
@@ -2388,6 +2390,12 @@ sub format_extended_diff_header_line {
my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
$line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
}
+ if (defined $mode) {
+ $line .= " $mode" .
+ '<span class="info"> (' .
+ file_type_long($mode) .
+ ')</span>';
+ }
return $line . "<br/>\n";
}
diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh
index 81d5625..85f7716 100755
--- a/t/t9502-gitweb-standalone-parse-output.sh
+++ b/t/t9502-gitweb-standalone-parse-output.sh
@@ -115,6 +115,19 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '
'
test_debug 'cat gitweb.headers'
+test_expect_success 'commitdiff: index line shortens hashes with mode' '
+ old_blob=$(git rev-parse HEAD:foo) &&
+ old_short=$(git rev-parse --short=7 HEAD:foo) &&
+ echo changed >foo &&
+ git commit -am "change foo" &&
+ new_blob=$(git rev-parse HEAD:foo) &&
+ new_short=$(git rev-parse --short=7 HEAD:foo) &&
+ gitweb_run "p=.git;a=commitdiff;h=HEAD" &&
+ test_grep ">${old_short}</a>\\.\\.<a [^>]*>${new_short}</a> 100644<span class=\"info\"> (file)</span>" \
+ gitweb.body &&
+ test_grep ! "index ${old_blob}\\.\\.${new_blob} 100644" gitweb.body
+'
+
# ----------------------------------------------------------------------
# forks of projects
--
2.52.0
^ permalink raw reply related
* Re: [PATCH 2/2] t9502: test gitweb index hash formatting with modes
From: Travor Liu @ 2026-07-17 13:42 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git@vger.kernel.org
In-Reply-To: <xmqqjyqu6u7i.fsf@gitster.g>
Thanks for the comments. I will squash the changes into one patch and use test_grep in v2.
Best,
Travor
On 2026/7/17, 1:50 PM, "Junio C Hamano" <gitster@pobox.com <mailto:gitster@pobox.com>> wrote:
Travor@web.codeaurora.org <mailto:Travor@web.codeaurora.org>, "Liu <travor_lzh"@outlook.com writes:
> From: Travor Liu <travor_lzh@outlook.com <mailto:travor_lzh@outlook.com>>
>
> gitweb should shorten and link the object IDs in commitdiff index lines
> even when Git includes the trailing file mode:
>
> index <old>..<new> 100644
>
> Add coverage for that common form by rendering a commitdiff for a
> regular file modification. Check that the visible index line contains
> linked short blob IDs followed by the mode and file-type annotation,
> and that the full unlinked form is not emitted.
>
> Signed-off-by: Travor Liu <travor_lzh@outlook.com <mailto:travor_lzh@outlook.com>>
> ---
> t/t9502-gitweb-standalone-parse-output.sh | 14 ++++++++++++++
> 1 file changed, 14 insertions(+)
If the new test added by this patch validates the "fix" in
[PATCH 1/2], reviewing the change might be easier if the two
were squashed into a single patch.
> diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh
> index 81d5625..7f37e26 100755
> --- a/t/t9502-gitweb-standalone-parse-output.sh
> +++ b/t/t9502-gitweb-standalone-parse-output.sh
> @@ -115,6 +115,20 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '
> '
> test_debug 'cat gitweb.headers'
>
> +test_expect_success 'commitdiff: index line shortens hashes with mode' '
> + old_blob=$(git rev-parse HEAD:foo) &&
> + old_short=$(git rev-parse --short=7 HEAD:foo) &&
> + echo changed >foo &&
> + git commit -am "change foo" &&
> + new_blob=$(git rev-parse HEAD:foo) &&
> + new_short=$(git rev-parse --short=7 HEAD:foo) &&
> + gitweb_run "p=.git;a=commitdiff;h=HEAD" &&
> + grep ">${old_short}</a>\\.\\.<a [^>]*>${new_short}</a> 100644" \
> + gitweb.body >index_line &&
> + grep "<span class=\"info\"> (file)</span>" index_line &&
> + ! grep "index ${old_blob}\\.\\.${new_blob} 100644" gitweb.body
> +'
Can we use "test_grep" (for positive "this string must be there") and
"test_grep !" (for negative "it is an error if this string appears"
(note that exclamation point comes after test_grep))? It would make
it easier to diagnose a failing test.
Also, there is a topic in flight that enforces the use of test_grep in
these test scripts, and use of raw grep like the above would break
under those stricter rules.
Thanks.
^ permalink raw reply
* Re: [PATCH 2/2] t9502: test gitweb index hash formatting with modes
From: Travor Liu @ 2026-07-17 13:41 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git@vger.kernel.org
In-Reply-To: <xmqqjyqu6u7i.fsf@gitster.g>
Thanks for the comments. I will squash the changes into one patch and use test_grep in v2.
Best,
Travor
On 2026/7/17, 1:50 PM, "Junio C Hamano" <gitster@pobox.com <mailto:gitster@pobox.com>> wrote:
Travor@web.codeaurora.org <mailto:Travor@web.codeaurora.org>, "Liu <travor_lzh"@outlook.com writes:
> From: Travor Liu <travor_lzh@outlook.com <mailto:travor_lzh@outlook.com>>
>
> gitweb should shorten and link the object IDs in commitdiff index lines
> even when Git includes the trailing file mode:
>
> index <old>..<new> 100644
>
> Add coverage for that common form by rendering a commitdiff for a
> regular file modification. Check that the visible index line contains
> linked short blob IDs followed by the mode and file-type annotation,
> and that the full unlinked form is not emitted.
>
> Signed-off-by: Travor Liu <travor_lzh@outlook.com <mailto:travor_lzh@outlook.com>>
> ---
> t/t9502-gitweb-standalone-parse-output.sh | 14 ++++++++++++++
> 1 file changed, 14 insertions(+)
If the new test added by this patch validates the "fix" in
[PATCH 1/2], reviewing the change might be easier if the two
were squashed into a single patch.
> diff --git a/t/t9502-gitweb-standalone-parse-output.sh b/t/t9502-gitweb-standalone-parse-output.sh
> index 81d5625..7f37e26 100755
> --- a/t/t9502-gitweb-standalone-parse-output.sh
> +++ b/t/t9502-gitweb-standalone-parse-output.sh
> @@ -115,6 +115,20 @@ test_expect_success 'snapshot: hierarchical branch name (xx/test)' '
> '
> test_debug 'cat gitweb.headers'
>
> +test_expect_success 'commitdiff: index line shortens hashes with mode' '
> + old_blob=$(git rev-parse HEAD:foo) &&
> + old_short=$(git rev-parse --short=7 HEAD:foo) &&
> + echo changed >foo &&
> + git commit -am "change foo" &&
> + new_blob=$(git rev-parse HEAD:foo) &&
> + new_short=$(git rev-parse --short=7 HEAD:foo) &&
> + gitweb_run "p=.git;a=commitdiff;h=HEAD" &&
> + grep ">${old_short}</a>\\.\\.<a [^>]*>${new_short}</a> 100644" \
> + gitweb.body >index_line &&
> + grep "<span class=\"info\"> (file)</span>" index_line &&
> + ! grep "index ${old_blob}\\.\\.${new_blob} 100644" gitweb.body
> +'
Can we use "test_grep" (for positive "this string must be there") and
"test_grep !" (for negative "it is an error if this string appears"
(note that exclamation point comes after test_grep))? It would make
it easier to diagnose a failing test.
Also, there is a topic in flight that enforces the use of test_grep in
these test scripts, and use of raw grep like the above would break
under those stricter rules.
Thanks.
^ permalink raw reply
* [GSoC Patch v2 7/7] repo: add path.git-prefix path key
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
Scripts and command-line prompt integrations frequently need to know their
relative depth inside a repository working tree layout. Currently, this
is retrieved using `git rev-parse --show-prefix`.
Introduce the `path.git-prefix` key to `git repo info`. This mirrors the
prefix location tracking framework as a standalone key, returning the
exact relative path offset complete with a trailing slash, or an empty
string if run directly at the repository working tree root.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 5 +++++
builtin/repo.c | 11 +++++++++++
t/t1900-repo-info.sh | 19 +++++++++++++++++++
3 files changed, 35 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 6c962620ec..5ba2ab1612 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -113,6 +113,11 @@ values that they return:
The path to the Git repository's common directory relative to
the current working directory.
+`path.git-prefix`::
+ The relative path from the top-level directory of the working tree to
+ the current working directory (including a trailing slash). Outputs an
+ empty string if executed at the root of the working tree.
+
`path.gitdir.absolute`::
The canonical absolute path to the Git repository directory (the `.git` directory).
diff --git a/builtin/repo.c b/builtin/repo.c
index a97ad71649..b93c140c74 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -100,6 +100,16 @@ static int get_path_commondir_relative(struct repository *repo, struct strbuf *b
return 0;
}
+static int get_path_git_prefix(struct repository *repo UNUSED, struct strbuf *buf)
+{
+ /*
+ * startup_info->prefix is NULL if we are at the working tree root.
+ * We add an empty string to ensure the buffer is cleanly initialized.
+ */
+ strbuf_addstr(buf, startup_info->prefix ? startup_info->prefix : "");
+ return 0;
+}
+
static int get_path_gitdir_absolute(struct repository *repo, struct strbuf *buf)
{
const char *git_dir = repo_get_git_dir(repo);
@@ -278,6 +288,7 @@ static const struct repo_info_field repo_info_field[] = {
{ "object.format", get_object_format },
{ "path.commondir.absolute", get_path_commondir_absolute },
{ "path.commondir.relative", get_path_commondir_relative },
+ { "path.git-prefix", get_path_git_prefix },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
{ "path.grafts.absolute", get_path_grafts_absolute },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 6c47989df7..3e5e42f6d3 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -207,6 +207,25 @@ test_repo_info_path 'commondir with only GIT_DIR' 'commondir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_expect_success 'path.git-prefix at root and in a subdirectory' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+
+ echo "path.git-prefix=" >expect.root &&
+ git repo info path.git-prefix >actual.root &&
+ test_cmp expect.root actual.root &&
+
+ mkdir -p sub/dir &&
+ cd sub/dir &&
+
+ echo "path.git-prefix=sub/dir/" >expect.sub &&
+ git repo info path.git-prefix >actual.sub &&
+ test_cmp expect.sub actual.sub
+ )
+'
+
test_repo_info_path 'gitdir standard' 'gitdir' '.git'
test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 6/7] repo: add path.grafts with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
External toolchains managing specialized history rewrites or legacy
history splices require access to the location of the repository grafts
file. Currently, this requires a legacy call to `git rev-parse --git-path info/grafts`.
Introduce `path.grafts.absolute` and `path.grafts.relative` keys to
`git repo info`. This allows scripting layers to query the active grafts
context cleanly while scaling transparently with active `GIT_GRAFT_FILE`
environment variable overrides.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 8 ++++++++
builtin/repo.c | 24 ++++++++++++++++++++++++
t/t1900-repo-info.sh | 6 ++++++
3 files changed, 38 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 3a837c573e..6c962620ec 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -119,6 +119,14 @@ values that they return:
`path.gitdir.relative`::
The path to the Git repository directory relative to the current working directory.
+`path.grafts.absolute`::
+ The canonical absolute path to the repository grafts file.
+ Respects the `GIT_GRAFT_FILE` environment override.
+
+`path.grafts.relative`::
+ The path to the repository grafts file relative to the current working
+ directory. Respects the `GIT_GRAFT_FILE` environment override.
+
`path.hooks.absolute`::
The canonical absolute path to the repository's hooks directory.
Respects `core.hooksPath` configuration adjustments.
diff --git a/builtin/repo.c b/builtin/repo.c
index 66bf4c67cc..a97ad71649 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -122,6 +122,28 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_grafts_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *graft_file = repo_get_graft_file(repo);
+
+ if (!graft_file)
+ return error(_("unable to get graft file"));
+
+ format_path(buf, graft_file, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_grafts_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *graft_file = repo_get_graft_file(repo);
+
+ if (!graft_file)
+ return error(_("unable to get graft file"));
+
+ format_path(buf, graft_file, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_path_hooks_absolute(struct repository *repo, struct strbuf *buf)
{
struct strbuf hooks_path = STRBUF_INIT;
@@ -258,6 +280,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
+ { "path.grafts.absolute", get_path_grafts_absolute },
+ { "path.grafts.relative", get_path_grafts_relative },
{ "path.hooks.absolute", get_path_hooks_absolute },
{ "path.hooks.relative", get_path_hooks_relative },
{ "path.index.absolute", get_path_index_absolute },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 04e6b8553c..6c47989df7 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -213,6 +213,12 @@ test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_repo_info_path 'grafts standard' 'grafts' '.git/info/grafts'
+
+test_repo_info_path 'grafts with GIT_GRAFT_FILE override' 'grafts' \
+ 'custom-graft-file' \
+ 'GIT_GRAFT_FILE="$ROOT/custom-graft-file" && export GIT_GRAFT_FILE'
+
test_repo_info_path 'hooks standard fallback' 'hooks' '.git/hooks'
test_repo_info_path 'hooks with core.hooksPath override' 'hooks' \
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 5/7] repo: add path.index with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
External script workflows and formatting layers require straightforward
access to the location of the index staging file. Currently, tracking
this necessitates a legacy call to `git rev-parse --git-path index` or
`--show-toplevel` logic abstractions.
Introduce `path.index.absolute` and `path.index.relative` keys to
`git repo info`. This allows tooling utilities to discover the active
index context cleanly while scaling transparently with localized
`GIT_INDEX_FILE` environment overrides.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 8 ++++++++
builtin/repo.c | 24 ++++++++++++++++++++++++
t/t1900-repo-info.sh | 6 ++++++
3 files changed, 38 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 7bc1c51310..3a837c573e 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -127,6 +127,14 @@ values that they return:
The path to the repository's hooks directory relative to the current
working directory. Respects `core.hooksPath` configuration adjustments.
+`path.index.absolute`::
+ The canonical absolute path to the repository's current index file.
+ Respects the `GIT_INDEX_FILE` environment override.
+
+`path.index.relative`::
+ The path to the repository's current index file relative to the current
+ working directory. Respects the `GIT_INDEX_FILE` environment override.
+
`path.objects.absolute`::
The canonical absolute path to the repository's object database directory.
Respects the `GIT_OBJECT_DIRECTORY` environment override.
diff --git a/builtin/repo.c b/builtin/repo.c
index c921de222d..66bf4c67cc 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -142,6 +142,28 @@ static int get_path_hooks_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_index_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *index_file = repo_get_index_file(repo);
+
+ if (!index_file)
+ return error(_("unable to get index file"));
+
+ format_path(buf, index_file, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_index_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *index_file = repo_get_index_file(repo);
+
+ if (!index_file)
+ return error(_("unable to get index file"));
+
+ format_path(buf, index_file, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_path_objects_absolute(struct repository *repo, struct strbuf *buf)
{
const char *obj_dir = repo_get_object_directory(repo);
@@ -238,6 +260,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.gitdir.relative", get_path_gitdir_relative },
{ "path.hooks.absolute", get_path_hooks_absolute },
{ "path.hooks.relative", get_path_hooks_relative },
+ { "path.index.absolute", get_path_index_absolute },
+ { "path.index.relative", get_path_index_relative },
{ "path.objects.absolute", get_path_objects_absolute },
{ "path.objects.relative", get_path_objects_relative },
{ "path.superproject-working-tree.absolute", get_path_superproject_absolute },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index cd3f856d04..04e6b8553c 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -219,6 +219,12 @@ test_repo_info_path 'hooks with core.hooksPath override' 'hooks' \
'custom-hooks' \
'git config core.hooksPath "$ROOT/custom-hooks" && mkdir -p "$ROOT/custom-hooks"'
+test_repo_info_path 'index standard' 'index' '.git/index'
+
+test_repo_info_path 'index with GIT_INDEX_FILE override' 'index' \
+ 'custom-index-file' \
+ 'GIT_INDEX_FILE="$ROOT/custom-index-file" && export GIT_INDEX_FILE'
+
test_repo_info_path 'objects standard' 'objects' '.git/objects'
test_repo_info_path 'objects with GIT_OBJECT_DIRECTORY override' 'objects' \
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 4/7] repo: add path.hooks with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
External tool integrations and validation systems need a stable way to
identify where the repository hooks are stored. Currently, this involves
relying on `git rev-parse --git-path hooks` or querying `core.hooksPath`
manually.
Introduce `path.hooks.absolute` and `path.hooks.relative` keys to
`git repo info`. This allows tools to discover the active hooks location
natively, ensuring proper resolution regardless of whether Git is using
the standard `.git/hooks` structure or a custom `core.hooksPath` setup.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 8 ++++++++
builtin/repo.c | 22 ++++++++++++++++++++++
t/t1900-repo-info.sh | 6 ++++++
3 files changed, 36 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 8429a44b43..7bc1c51310 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -119,6 +119,14 @@ values that they return:
`path.gitdir.relative`::
The path to the Git repository directory relative to the current working directory.
+`path.hooks.absolute`::
+ The canonical absolute path to the repository's hooks directory.
+ Respects `core.hooksPath` configuration adjustments.
+
+`path.hooks.relative`::
+ The path to the repository's hooks directory relative to the current
+ working directory. Respects `core.hooksPath` configuration adjustments.
+
`path.objects.absolute`::
The canonical absolute path to the repository's object database directory.
Respects the `GIT_OBJECT_DIRECTORY` environment override.
diff --git a/builtin/repo.c b/builtin/repo.c
index d6bdd5bcfa..c921de222d 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -122,6 +122,26 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_hooks_absolute(struct repository *repo, struct strbuf *buf)
+{
+ struct strbuf hooks_path = STRBUF_INIT;
+
+ repo_git_path_replace(repo, &hooks_path, "hooks");
+ format_path(buf, hooks_path.buf, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ strbuf_release(&hooks_path);
+ return 0;
+}
+
+static int get_path_hooks_relative(struct repository *repo, struct strbuf *buf)
+{
+ struct strbuf hooks_path = STRBUF_INIT;
+
+ repo_git_path_replace(repo, &hooks_path, "hooks");
+ format_path(buf, hooks_path.buf, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ strbuf_release(&hooks_path);
+ return 0;
+}
+
static int get_path_objects_absolute(struct repository *repo, struct strbuf *buf)
{
const char *obj_dir = repo_get_object_directory(repo);
@@ -216,6 +236,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
+ { "path.hooks.absolute", get_path_hooks_absolute },
+ { "path.hooks.relative", get_path_hooks_relative },
{ "path.objects.absolute", get_path_objects_absolute },
{ "path.objects.relative", get_path_objects_relative },
{ "path.superproject-working-tree.absolute", get_path_superproject_absolute },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 260f4fde43..cd3f856d04 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -213,6 +213,12 @@ test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_repo_info_path 'hooks standard fallback' 'hooks' '.git/hooks'
+
+test_repo_info_path 'hooks with core.hooksPath override' 'hooks' \
+ 'custom-hooks' \
+ 'git config core.hooksPath "$ROOT/custom-hooks" && mkdir -p "$ROOT/custom-hooks"'
+
test_repo_info_path 'objects standard' 'objects' '.git/objects'
test_repo_info_path 'objects with GIT_OBJECT_DIRECTORY override' 'objects' \
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 3/7] repo: add path.objects with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
Tools and deployment hooks frequently query the location of the object
database directory. Currently, this relies on legacy parsing methods or
manually inspecting `git rev-parse --git-path objects`.
Introduce `path.objects.absolute` and `path.objects.relative` keys to
`git repo info`. This allows tools to discover the object database
location safely while natively adhering to active `GIT_OBJECT_DIRECTORY`
environment variable overrides.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 9 +++++++++
builtin/repo.c | 24 ++++++++++++++++++++++++
t/t1900-repo-info.sh | 7 +++++++
3 files changed, 40 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index 03aa57942f..8429a44b43 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -119,6 +119,15 @@ values that they return:
`path.gitdir.relative`::
The path to the Git repository directory relative to the current working directory.
+`path.objects.absolute`::
+ The canonical absolute path to the repository's object database directory.
+ Respects the `GIT_OBJECT_DIRECTORY` environment override.
+
+`path.objects.relative`::
+ The path to the repository's object database directory relative to the
+ current working directory. Respects the `GIT_OBJECT_DIRECTORY`
+ environment override.
+
`path.superproject-working-tree.absolute`::
The canonical absolute path to the working tree root of the superproject
if the current repository is an initialized submodule. Outputs an empty
diff --git a/builtin/repo.c b/builtin/repo.c
index 82359473e9..d6bdd5bcfa 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -122,6 +122,28 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_objects_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *obj_dir = repo_get_object_directory(repo);
+
+ if (!obj_dir)
+ return error(_("unable to get object directory"));
+
+ format_path(buf, obj_dir, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_objects_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *obj_dir = repo_get_object_directory(repo);
+
+ if (!obj_dir)
+ return error(_("unable to get object directory"));
+
+ format_path(buf, obj_dir, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_path_superproject_absolute(struct repository *repo UNUSED, struct strbuf *buf)
{
struct strbuf superproject = STRBUF_INIT;
@@ -194,6 +216,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
+ { "path.objects.absolute", get_path_objects_absolute },
+ { "path.objects.relative", get_path_objects_relative },
{ "path.superproject-working-tree.absolute", get_path_superproject_absolute },
{ "path.superproject-working-tree.relative", get_path_superproject_relative },
{ "path.toplevel.absolute", get_path_toplevel_absolute },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index 220b3d4d3d..260f4fde43 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -213,6 +213,13 @@ test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_repo_info_path 'objects standard' 'objects' '.git/objects'
+
+test_repo_info_path 'objects with GIT_OBJECT_DIRECTORY override' 'objects' \
+ 'custom-objects' \
+ 'GIT_OBJECT_DIRECTORY="$ROOT/custom-objects" && export GIT_OBJECT_DIRECTORY &&
+ mkdir -p "$ROOT/custom-objects"'
+
test_expect_success 'path.superproject-working-tree absolute and relative' '
test_when_finished "rm -rf sub super" &&
git init sub &&
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 2/7] repo: add path.superproject-working-tree with absolute and relative suffixes
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
Scripts working in multi-repository setups often need to identify the
top-level working tree of a superproject from within a submodule.
Currently, this is only exposed via `git rev-parse
--show-superproject-working-tree`.
Introduce `path.superproject-working-tree.absolute` and
`path.superproject-working-tree.relative` keys to `git repo info`.
This exposes the core submodule context via a scriptable config-like key
using standard format rules.
If requested when not inside a submodule, the command returns an empty
string.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 10 ++++++++++
builtin/repo.c | 33 +++++++++++++++++++++++++++++++++
t/t1900-repo-info.sh | 34 ++++++++++++++++++++++++++++++++++
3 files changed, 77 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index e34abe5fea..03aa57942f 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -119,6 +119,16 @@ values that they return:
`path.gitdir.relative`::
The path to the Git repository directory relative to the current working directory.
+`path.superproject-working-tree.absolute`::
+ The canonical absolute path to the working tree root of the superproject
+ if the current repository is an initialized submodule. Outputs an empty
+ string if not in a submodule.
+
+`path.superproject-working-tree.relative`::
+ The path to the working tree root of the superproject relative to the
+ current working directory if the current repository is an initialized
+ submodule. Outputs an empty string if not in a submodule.
+
`path.toplevel.absolute`::
The canonical absolute path to the top-level directory of the
repository's working tree. Outputs an empty string if the repository
diff --git a/builtin/repo.c b/builtin/repo.c
index 194757eb18..82359473e9 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -18,6 +18,7 @@
#include "strbuf.h"
#include "string-list.h"
#include "shallow.h"
+#include "submodule.h"
#include "tree.h"
#include "tree-walk.h"
#include "utf8.h"
@@ -121,6 +122,36 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_superproject_absolute(struct repository *repo UNUSED, struct strbuf *buf)
+{
+ struct strbuf superproject = STRBUF_INIT;
+
+ if (!get_superproject_working_tree(&superproject)) {
+ strbuf_release(&superproject);
+ strbuf_addstr(buf, "");
+ return 0;
+ }
+
+ format_path(buf, superproject.buf, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ strbuf_release(&superproject);
+ return 0;
+}
+
+static int get_path_superproject_relative(struct repository *repo UNUSED, struct strbuf *buf)
+{
+ struct strbuf superproject = STRBUF_INIT;
+
+ if (!get_superproject_working_tree(&superproject)) {
+ strbuf_release(&superproject);
+ strbuf_addstr(buf, "");
+ return 0;
+ }
+
+ format_path(buf, superproject.buf, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ strbuf_release(&superproject);
+ return 0;
+}
+
static int get_path_toplevel_absolute(struct repository *repo, struct strbuf *buf)
{
const char *work_tree = repo_get_work_tree(repo);
@@ -163,6 +194,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
+ { "path.superproject-working-tree.absolute", get_path_superproject_absolute },
+ { "path.superproject-working-tree.relative", get_path_superproject_relative },
{ "path.toplevel.absolute", get_path_toplevel_absolute },
{ "path.toplevel.relative", get_path_toplevel_relative },
{ "references.format", get_references_format },
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index fbb9063ee5..220b3d4d3d 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -213,6 +213,40 @@ test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_expect_success 'path.superproject-working-tree absolute and relative' '
+ test_when_finished "rm -rf sub super" &&
+ git init sub &&
+ test_commit -C sub initial &&
+ git init super &&
+ (
+ cd super &&
+ git -c protocol.file.allow=always submodule add "../sub" sub &&
+ git commit -m "add submodule" &&
+
+ cd sub &&
+ ROOT="$(test-tool path-utils real_path ..)" &&
+
+ echo "path.superproject-working-tree.absolute=$ROOT" >expect.abs &&
+ git repo info path.superproject-working-tree.absolute >actual.abs &&
+ test_cmp expect.abs actual.abs &&
+
+ echo "path.superproject-working-tree.relative=../" >expect.rel &&
+ git repo info path.superproject-working-tree.relative >actual.rel &&
+ test_cmp expect.rel actual.rel
+ )
+'
+
+test_expect_success 'path.superproject-working-tree returns empty when not in a submodule' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ echo "path.superproject-working-tree.absolute=" >expect &&
+ git repo info path.superproject-working-tree.absolute >actual &&
+ test_cmp expect actual
+ )
+'
+
test_expect_success 'path.toplevel absolute and relative' '
test_when_finished "rm -rf repo" &&
git init repo &&
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 1/7] repo: add path.toplevel with absolute and relative suffix formatting
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260717133015.32040-1-jayatheerthkulkarni2005@gmail.com>
Scripts frequently need to find the root directory of a repository's
working tree. Currently, this requires using `git rev-parse --show-toplevel`
or inferring it from other path components.
Introduce `path.toplevel.absolute` and `path.toplevel.relative` keys
to `git repo info`. This allows scripts to retrieve the top-level
working tree path in a predictable, strictly formatted manner without
relying on `rev-parse`.
If requested in a bare repository where no working tree exists, the
command returns an empty string.
Mentored-by: Justin Tobler <jltobler@gmail.com>
Mentored-by: Lucas Seiki Oshiro <lucasseikioshiro@gmail.com>
Signed-off-by: K Jayatheerth <jayatheerthkulkarni2005@gmail.com>
---
Documentation/git-repo.adoc | 10 ++++++++++
builtin/repo.c | 28 ++++++++++++++++++++++++++++
t/t1900-repo-info.sh | 30 ++++++++++++++++++++++++++++++
3 files changed, 68 insertions(+)
diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc
index ed7d80c690..e34abe5fea 100644
--- a/Documentation/git-repo.adoc
+++ b/Documentation/git-repo.adoc
@@ -119,6 +119,16 @@ values that they return:
`path.gitdir.relative`::
The path to the Git repository directory relative to the current working directory.
+`path.toplevel.absolute`::
+ The canonical absolute path to the top-level directory of the
+ repository's working tree. Outputs an empty string if the repository
+ is bare.
+
+`path.toplevel.relative`::
+ The path to the top-level directory of the repository's working
+ tree relative to the current working directory. Outputs an empty
+ string if the repository is bare.
+
`references.format`::
The reference storage format. The valid values are:
+
diff --git a/builtin/repo.c b/builtin/repo.c
index 042d6de558..194757eb18 100644
--- a/builtin/repo.c
+++ b/builtin/repo.c
@@ -121,6 +121,32 @@ static int get_path_gitdir_relative(struct repository *repo, struct strbuf *buf)
return 0;
}
+static int get_path_toplevel_absolute(struct repository *repo, struct strbuf *buf)
+{
+ const char *work_tree = repo_get_work_tree(repo);
+
+ if (!work_tree) {
+ strbuf_addstr(buf, "");
+ return 0;
+ }
+
+ format_path(buf, work_tree, startup_info->prefix, PATH_FORMAT_CANONICAL);
+ return 0;
+}
+
+static int get_path_toplevel_relative(struct repository *repo, struct strbuf *buf)
+{
+ const char *work_tree = repo_get_work_tree(repo);
+
+ if (!work_tree) {
+ strbuf_addstr(buf, "");
+ return 0;
+ }
+
+ format_path(buf, work_tree, startup_info->prefix, PATH_FORMAT_RELATIVE);
+ return 0;
+}
+
static int get_references_format(struct repository *repo, struct strbuf *buf)
{
strbuf_addstr(buf,
@@ -137,6 +163,8 @@ static const struct repo_info_field repo_info_field[] = {
{ "path.commondir.relative", get_path_commondir_relative },
{ "path.gitdir.absolute", get_path_gitdir_absolute },
{ "path.gitdir.relative", get_path_gitdir_relative },
+ { "path.toplevel.absolute", get_path_toplevel_absolute },
+ { "path.toplevel.relative", get_path_toplevel_relative },
{ "references.format", get_references_format },
};
diff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh
index ae8c22c817..fbb9063ee5 100755
--- a/t/t1900-repo-info.sh
+++ b/t/t1900-repo-info.sh
@@ -213,4 +213,34 @@ test_repo_info_path 'gitdir with explicit GIT_DIR' 'gitdir' \
'.git' \
'GIT_DIR="../.git" && export GIT_DIR'
+test_expect_success 'path.toplevel absolute and relative' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ mkdir -p repo/sub &&
+ cd repo/sub &&
+
+ ROOT="$(test-tool path-utils real_path ..)" &&
+
+ echo "path.toplevel.absolute=$ROOT" >expect.abs &&
+ git repo info path.toplevel.absolute >actual.abs &&
+ test_cmp expect.abs actual.abs &&
+
+ echo "path.toplevel.relative=../" >expect.rel &&
+ git repo info path.toplevel.relative >actual.rel &&
+ test_cmp expect.rel actual.rel
+ )
+'
+
+test_expect_success 'path.toplevel returns empty in a bare repository' '
+ test_when_finished "rm -rf bare.git" &&
+ git init --bare bare.git &&
+ (
+ cd bare.git &&
+ echo "path.toplevel.absolute=" >expect &&
+ git repo info path.toplevel.absolute >actual &&
+ test_cmp expect actual
+ )
+'
+
test_done
--
2.55.GIT
^ permalink raw reply related
* [GSoC Patch v2 0/7] repo: add more path keys to git repo info
From: K Jayatheerth @ 2026-07-17 13:30 UTC (permalink / raw)
To: jayatheerthkulkarni2005; +Cc: git, jltobler, lucasseikioshiro
In-Reply-To: <20260716012138.6714-1-jayatheerthkulkarni2005@gmail.com>
Series adds keys to git repo info.
Keys output paths of repository components:
* path.toplevel: repository tree.
* path.superproject-working-tree: superproject tree from submodules.
* path.objects: repository objects.
* path.hooks: repository hooks.
* path.index: repository index.
* path.grafts: repository grafts.
* path.git-prefix: prefix offset.
Keys support suffixes for format.
Commits contain documentation and tests.
K Jayatheerth (7):
repo: add path.toplevel with absolute and relative suffix formatting
repo: add path.superproject-working-tree with absolute and relative
suffixes
repo: add path.objects with absolute and relative suffix formatting
repo: add path.hooks with absolute and relative suffix formatting
repo: add path.index with absolute and relative suffix formatting
repo: add path.grafts with absolute and relative suffix formatting
repo: add path.git-prefix path key
Documentation/git-repo.adoc | 58 +++++++++++++
builtin/repo.c | 166 ++++++++++++++++++++++++++++++++++++
t/t1900-repo-info.sh | 108 +++++++++++++++++++++++
3 files changed, 332 insertions(+)
--
2.55.GIT
^ permalink raw reply
* Re: [PATCH 1/6] SubmittingPatches: clarify expected structure of commit log message
From: Weijie Yuan @ 2026-07-17 12:42 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: Junio C Hamano, Michael Montalbo, git
In-Reply-To: <CALnO6CD8HFWaeN-4Gccopy0nw601cMyak_LSXfTsAa8xwOjKpQ@mail.gmail.com>
On Tue, Jul 14, 2026 at 06:46:05PM -0400, D. Ben Knoble wrote:
> On Mon, Jul 13, 2026 at 10:42 AM Weijie Yuan <wy@wyuan.org> wrote:
> >
> [snip]
> > I think this might confuse readers. Now you place these points in
> > parallel:
> >
> > 1. Title
> > 2. Body
> > 3. Observation (The Status Quo)
> > 4. Solution Design (The Approach)
> > 5. Implementation (The Execution)
>
> Without commenting on "confuse," I find this style of heading
>
> Thing (The Other Thing)
>
> needlessly suggests an LLM's involvement with the text.
Aha, kind of. But I guess Junio didn't use LLM here ;-)
> That by itself is not grounds for my objection; instead, I'll note
> that often the parenthetical restates the original header in some way.
> That makes it redundant. (In some cases in the wild I have seen
> examples where the 2 were not synonymous, which _is_ confusing :)
True.
> > But acatually you mean:
> >
> > 1. Title
> > 2. Body
> > The body typically follows three parts:
> > a. Observation
> > b. Solution Design
> > c. Implementation
> >
> > But I haven't written much about adoc, so I don't know its syntax and
> > how to write it.
>
> This is nice. If I had to suggest anything further, it would be "don't
> be afraid of long headings":
>
> 1. Title: Summarize the change
> 2. Body: Describe [Justify?] the change
> a. Observe the status quo
> b. Explain your approach [solution/design/etc.]
> c. Command the code to change [or: Describe the implementation/execution]
>
> ?
I agree. More explanatory descriptions here are very likely to enable
contributors to express their ideas more clearly and understandably.
^ permalink raw reply
* Re: [PATCH 2/6] MyFirstContribution: what if I don't get a reply?
From: Patrick Steinhardt @ 2026-07-17 11:12 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
In-Reply-To: <20260711192650.2417665-3-gitster@pobox.com>
On Sat, Jul 11, 2026 at 12:26:46PM -0700, Junio C Hamano wrote:
> Tell readers that pinging is a perfectly sensible thing to do when
> they do not see a response.
>
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
> Documentation/MyFirstContribution.adoc | 13 +++++++++++++
> 1 file changed, 13 insertions(+)
>
> diff --git a/Documentation/MyFirstContribution.adoc b/Documentation/MyFirstContribution.adoc
> index 4832e5bad5..fc2ce2e785 100644
> --- a/Documentation/MyFirstContribution.adoc
> +++ b/Documentation/MyFirstContribution.adoc
> @@ -1438,6 +1438,19 @@ substantial rework, and mention which parts of the current series will become
> obsolete so reviewers can avoid spending time on them until the updated series
> is ready.
>
> +=== What if I don't get a reply?
> +
> +If you don't receive any review comments after a week or two, do not
> +assume your patch has been accepted or merged. In the Git project,
> +silence does not equal approval. It usually means reviewers are busy
> +or haven't noticed your contribution.
Should we also add the third reason: reviewers are simply not interested
in the patch? It's a bit brutal, but that's quite a common reason, too.
In the best case we'd of course tell the submitter that we don't want
the patch to not leave them hanging.
> +If your patch is overlooked, it is perfectly acceptable to send a
> +polite ping to the thread. You can do this by replying to your own
> +cover letter (or patch) to ask if anyone has had a chance to look at
> +it. You can also CC additional people who might be interested; use
> +the `git-contacts` script (mentioned earlier) to find relevant contributors.
And this paragraph here can remain as-is regardless of which of the
three reasons applies.
Patrick
^ permalink raw reply
* Re: [PATCH v7] show-branch: convert per-branch flags to commit-slab
From: Patrick Steinhardt @ 2026-07-17 10:42 UTC (permalink / raw)
To: Gatla Vishweshwar Reddy; +Cc: gitster, git
In-Reply-To: <20260717103454.62750-1-gatlavishweshwarreddy26@gmail.com>
Hi Gatla,
On Fri, Jul 17, 2026 at 04:04:54PM +0530, Gatla Vishweshwar Reddy wrote:
> Hi Patrick,
>
> I am a real person. I used AI help for structuring reply in that thread. I understand that is not
> appropriate here and will write my own from now on.
Okay. Using AI is fine to help you out, but the human-focussed bits
should really rather be written in a way that it feels like we're
talking to a human. We're a community here, and when you see text that
is so obviously written by an AI it can get very frustrating eventually.
We've seen a strong uptick in threads that are obviously AI generated,
only, and at times it just feels like one is merely talking to a prompt.
This just doesn't scale well, as it leads to constant iterations and
back and forth without much thinking being involved. So we require the
other side to stop every once in a while and invest the necessary time,
too. Otherwise the community will simply stop working, and that doesn't
serve anyone well.
Sorry if I came across as harsh.
Patrick
^ permalink raw reply
* Re: [PATCH v7] show-branch: convert per-branch flags to commit-slab
From: Gatla Vishweshwar Reddy @ 2026-07-17 10:34 UTC (permalink / raw)
To: ps; +Cc: gitster, git, Gatla Vishweshwar Reddy
In-Reply-To: <alntPJy2VwVK75qj@pks.im>
Hi Patrick,
I am a real person. I used AI help for structuring reply in that thread. I understand that is not
appropriate here and will write my own from now on.
Vishweshwar
^ permalink raw reply
* [PATCH 9/9] object-file: move logic to write loose objects
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
The logic to write loose objects is split up across "object-file.c" and
"odb/source-loose.c". This split is somewhat weird, but it is the result
of two things:
- `force_object_loose()` used to reach into internals of how exactly
we write objects.
- The logic of writing objects is intertwined with potentially
starting a transaction.
We have refactored `force_object_loose()` over preceding commits to work
via generic interfaces now, so this reason doesn't exist anymore. But
the second reason still does, as our management of "files" transactions
and their ad-hoc creation is still very messy. This area definitely
requires further work, and that work is indeed ongoing.
That being said, we can already move the writing logic into the "loose"
backend rather easily. All we have to do is to expose two functions that
relate to the transactions.
Expose these two functions and move the writing logic into the "loose"
backend accordingly so that it becomes more self-contained. Note that
this requires us to drop a reference to `the_repository` in favor of
using the source's repository in `start_loose_object_common()`.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
object-file.c | 360 +----------------------------------------------------
object-file.h | 22 +---
odb/source-loose.c | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 357 insertions(+), 379 deletions(-)
diff --git a/object-file.c b/object-file.c
index b867d8d9de..bdc97d7943 100644
--- a/object-file.c
+++ b/object-file.c
@@ -491,7 +491,7 @@ struct odb_transaction_files {
const char *prefix;
};
-static int odb_transaction_files_prepare(struct odb_transaction *base)
+int odb_transaction_files_prepare(struct odb_transaction *base)
{
struct odb_transaction_files *transaction =
container_of_or_null(base, struct odb_transaction_files, base);
@@ -514,8 +514,8 @@ static int odb_transaction_files_prepare(struct odb_transaction *base)
return 0;
}
-static void odb_transaction_files_fsync(struct odb_transaction *base,
- int fd, const char *filename)
+void odb_transaction_files_fsync(struct odb_transaction *base,
+ int fd, const char *filename)
{
struct odb_transaction_files *transaction =
container_of_or_null(base, struct odb_transaction_files, base);
@@ -539,360 +539,6 @@ static void odb_transaction_files_fsync(struct odb_transaction *base,
}
}
-/* Finalize a file on disk, and close it. */
-static void close_loose_object(struct odb_source_loose *loose,
- int fd, const char *filename)
-{
- if (loose->base.will_destroy)
- goto out;
-
- if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
- odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename);
- else if (fsync_object_files > 0)
- fsync_or_die(fd, filename);
- else
- fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,
- filename);
-
-out:
- if (close(fd) != 0)
- die_errno(_("error when closing loose object file"));
-}
-
-/* Size of directory component, including the ending '/' */
-static inline int directory_size(const char *filename)
-{
- const char *s = strrchr(filename, '/');
- if (!s)
- return 0;
- return s - filename + 1;
-}
-
-/*
- * This creates a temporary file in the same directory as the final
- * 'filename'
- *
- * We want to avoid cross-directory filename renames, because those
- * can have problems on various filesystems (FAT, NFS, Coda).
- */
-static int create_tmpfile(struct repository *repo,
- struct strbuf *tmp, const char *filename)
-{
- int fd, dirlen = directory_size(filename);
-
- strbuf_reset(tmp);
- strbuf_add(tmp, filename, dirlen);
- strbuf_addstr(tmp, "tmp_obj_XXXXXX");
- fd = git_mkstemp_mode(tmp->buf, 0444);
- if (fd < 0 && dirlen && errno == ENOENT) {
- /*
- * Make sure the directory exists; note that the contents
- * of the buffer are undefined after mkstemp returns an
- * error, so we have to rewrite the whole buffer from
- * scratch.
- */
- strbuf_reset(tmp);
- strbuf_add(tmp, filename, dirlen - 1);
- if (mkdir(tmp->buf, 0777) && errno != EEXIST)
- return -1;
- if (adjust_shared_perm(repo, tmp->buf))
- return -1;
-
- /* Try again */
- strbuf_addstr(tmp, "/tmp_obj_XXXXXX");
- fd = git_mkstemp_mode(tmp->buf, 0444);
- }
- return fd;
-}
-
-/**
- * Common steps for loose object writers to start writing loose
- * objects:
- *
- * - Create tmpfile for the loose object.
- * - Setup zlib stream for compression.
- * - Start to feed header to zlib stream.
- *
- * Returns a "fd", which should later be provided to
- * end_loose_object_common().
- */
-static int start_loose_object_common(struct odb_source_loose *loose,
- struct strbuf *tmp_file,
- const char *filename, unsigned flags,
- git_zstream *stream,
- unsigned char *buf, size_t buflen,
- struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
- char *hdr, int hdrlen)
-{
- const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;
- const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
- int fd;
- struct repo_config_values *cfg = repo_config_values(the_repository);
-
- fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);
- if (fd < 0) {
- if (flags & ODB_WRITE_OBJECT_SILENT)
- return -1;
- else if (errno == EACCES)
- return error(_("insufficient permission for adding "
- "an object to repository database %s"),
- loose->base.path);
- else
- return error_errno(
- _("unable to create temporary file"));
- }
-
- /* Setup zlib stream for compression */
- git_deflate_init(stream, cfg->zlib_compression_level);
- stream->next_out = buf;
- stream->avail_out = buflen;
- git_hash_init(c, algo);
- if (compat && compat_c)
- git_hash_init(compat_c, compat);
-
- /* Start to feed header to zlib stream */
- stream->next_in = (unsigned char *)hdr;
- stream->avail_in = hdrlen;
- while (git_deflate(stream, 0) == Z_OK)
- ; /* nothing */
- git_hash_update(c, hdr, hdrlen);
- if (compat && compat_c)
- git_hash_update(compat_c, hdr, hdrlen);
-
- return fd;
-}
-
-/**
- * Common steps for the inner git_deflate() loop for writing loose
- * objects. Returns what git_deflate() returns.
- */
-static int write_loose_object_common(struct odb_source_loose *loose,
- struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
- git_zstream *stream, const int flush,
- unsigned char *in0, const int fd,
- unsigned char *compressed,
- const size_t compressed_len)
-{
- const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
- int ret;
-
- ret = git_deflate(stream, flush ? Z_FINISH : 0);
- git_hash_update(c, in0, stream->next_in - in0);
- if (compat && compat_c)
- git_hash_update(compat_c, in0, stream->next_in - in0);
- if (write_in_full(fd, compressed, stream->next_out - compressed) < 0)
- die_errno(_("unable to write loose object file"));
- stream->next_out = compressed;
- stream->avail_out = compressed_len;
-
- return ret;
-}
-
-/**
- * Common steps for loose object writers to end writing loose objects:
- *
- * - End the compression of zlib stream.
- * - Get the calculated oid to "oid".
- */
-static int end_loose_object_common(struct odb_source_loose *loose,
- struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
- git_zstream *stream, struct object_id *oid,
- struct object_id *compat_oid)
-{
- const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
- int ret;
-
- ret = git_deflate_end_gently(stream);
- if (ret != Z_OK)
- return ret;
- git_hash_final_oid(oid, c);
- if (compat && compat_c)
- git_hash_final_oid(compat_oid, compat_c);
-
- return Z_OK;
-}
-
-int write_loose_object(struct odb_source_loose *loose,
- const struct object_id *oid, char *hdr,
- int hdrlen, const void *buf, unsigned long len,
- const time_t *mtime, unsigned flags)
-{
- int fd, ret;
- unsigned char compressed[4096];
- git_zstream stream;
- struct git_hash_ctx c;
- struct object_id parano_oid;
- static struct strbuf tmp_file = STRBUF_INIT;
- static struct strbuf filename = STRBUF_INIT;
-
- if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
- odb_transaction_files_prepare(loose->base.odb->transaction);
-
- odb_loose_path(loose, &filename, oid);
-
- fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,
- &stream, compressed, sizeof(compressed),
- &c, NULL, hdr, hdrlen);
- if (fd < 0)
- return -1;
-
- /* Then the data itself.. */
- stream.next_in = (void *)buf;
- stream.avail_in = len;
- do {
- unsigned char *in0 = stream.next_in;
-
- ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,
- compressed, sizeof(compressed));
- } while (ret == Z_OK);
-
- if (ret != Z_STREAM_END)
- die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid),
- ret);
- ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL);
- if (ret != Z_OK)
- die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid),
- ret);
- if (!oideq(oid, ¶no_oid))
- die(_("confused by unstable object source data for %s"),
- oid_to_hex(oid));
-
- close_loose_object(loose, fd, tmp_file.buf);
-
- if (mtime) {
- struct utimbuf utb = {
- .actime = *mtime,
- .modtime = *mtime,
- };
-
- if (utime(tmp_file.buf, &utb) < 0 &&
- !(flags & ODB_WRITE_OBJECT_SILENT))
- warning_errno(_("failed utime() on %s"), tmp_file.buf);
- }
-
- return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
- FOF_SKIP_COLLISION_CHECK);
-}
-
-int odb_source_loose_write_stream(struct odb_source_loose *loose,
- struct odb_write_stream *in_stream, size_t len,
- struct object_id *oid)
-{
- const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
- struct object_id compat_oid;
- int fd, ret, err = 0, flush = 0;
- unsigned char compressed[4096];
- git_zstream stream;
- struct git_hash_ctx c, compat_c;
- struct strbuf tmp_file = STRBUF_INIT;
- struct strbuf filename = STRBUF_INIT;
- unsigned char buf[8192];
- int dirlen;
- char hdr[MAX_HEADER_LEN];
- int hdrlen;
-
- if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
- odb_transaction_files_prepare(loose->base.odb->transaction);
-
- /* Since oid is not determined, save tmp file to odb path. */
- strbuf_addf(&filename, "%s/", loose->base.path);
- hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);
-
- /*
- * Common steps for write_loose_object and stream_loose_object to
- * start writing loose objects:
- *
- * - Create tmpfile for the loose object.
- * - Setup zlib stream for compression.
- * - Start to feed header to zlib stream.
- */
- fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,
- &stream, compressed, sizeof(compressed),
- &c, &compat_c, hdr, hdrlen);
- if (fd < 0) {
- err = -1;
- goto cleanup;
- }
-
- /* Then the data itself.. */
- do {
- unsigned char *in0 = stream.next_in;
-
- if (!stream.avail_in && !in_stream->is_finished) {
- ssize_t read_len = odb_write_stream_read(in_stream, buf,
- sizeof(buf));
- if (read_len < 0) {
- close(fd);
- err = -1;
- goto cleanup;
- }
-
- stream.avail_in = read_len;
- stream.next_in = buf;
- in0 = buf;
- /* All data has been read. */
- if (in_stream->is_finished)
- flush = 1;
- }
- ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,
- compressed, sizeof(compressed));
- /*
- * Unlike write_loose_object(), we do not have the entire
- * buffer. If we get Z_BUF_ERROR due to too few input bytes,
- * then we'll replenish them in the next input_stream->read()
- * call when we loop.
- */
- } while (ret == Z_OK || ret == Z_BUF_ERROR);
-
- if (stream.total_in != len + hdrlen)
- die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in,
- (uintmax_t)len + hdrlen);
-
- /*
- * Common steps for write_loose_object and stream_loose_object to
- * end writing loose object:
- *
- * - End the compression of zlib stream.
- * - Get the calculated oid.
- */
- if (ret != Z_STREAM_END)
- die(_("unable to stream deflate new object (%d)"), ret);
- ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);
- if (ret != Z_OK)
- die(_("deflateEnd on stream object failed (%d)"), ret);
- close_loose_object(loose, fd, tmp_file.buf);
-
- if (odb_freshen_object(loose->base.odb, oid)) {
- unlink_or_warn(tmp_file.buf);
- goto cleanup;
- }
- odb_loose_path(loose, &filename, oid);
-
- /* We finally know the object path, and create the missing dir. */
- dirlen = directory_size(filename.buf);
- if (dirlen) {
- struct strbuf dir = STRBUF_INIT;
- strbuf_add(&dir, filename.buf, dirlen);
-
- if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&
- errno != EEXIST) {
- err = error_errno(_("unable to create directory %s"), dir.buf);
- strbuf_release(&dir);
- goto cleanup;
- }
- strbuf_release(&dir);
- }
-
- err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
- FOF_SKIP_COLLISION_CHECK);
- if (!err && compat)
- err = repo_add_loose_object_map(loose, oid, &compat_oid);
-cleanup:
- strbuf_release(&tmp_file);
- strbuf_release(&filename);
- return err;
-}
-
/*
* We can't use the normal fsck_error_function() for index_mem(),
* because we don't yet have a valid oid for it to report. Instead,
diff --git a/object-file.h b/object-file.h
index 31781a9c53..805f2cfa28 100644
--- a/object-file.h
+++ b/object-file.h
@@ -24,20 +24,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa
struct object_info;
struct odb_source;
-/*
- * Write the given stream into the loose object source. The only difference
- * from the generic implementation of this function is that we don't perform an
- * object existence check here.
- *
- * TODO: We should stop exposing this function altogether and move it into
- * "odb/source-loose.c". This requires a couple of refactorings though to make
- * `force_object_loose()` generic and is thus postponed to a later point in
- * time.
- */
-int odb_source_loose_write_stream(struct odb_source_loose *source,
- struct odb_write_stream *stream, size_t len,
- struct object_id *oid);
-
/*
* Put in `buf` the name of the file in the local object database that
* would be used to store a loose object with the specified oid.
@@ -131,10 +117,6 @@ int finalize_object_file_flags(struct repository *repo,
void hash_object_file(const struct git_hash_algo *algo, const void *buf,
size_t len, enum object_type type,
struct object_id *oid);
-int write_loose_object(struct odb_source_loose *loose,
- const struct object_id *oid, char *hdr,
- int hdrlen, const void *buf, unsigned long len,
- const time_t *mtime, unsigned flags);
/* Helper to check and "touch" a file */
int check_and_freshen_file(const char *fn, int freshen,
@@ -195,4 +177,8 @@ int odb_transaction_files_begin(struct odb_source *source,
struct odb_transaction **out,
enum odb_transaction_flags flags);
+int odb_transaction_files_prepare(struct odb_transaction *base);
+void odb_transaction_files_fsync(struct odb_transaction *base,
+ int fd, const char *filename);
+
#endif /* OBJECT_FILE_H */
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 520a30157c..ef0e919277 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -11,8 +11,11 @@
#include "odb/source-loose.h"
#include "odb/streaming.h"
#include "oidtree.h"
+#include "path.h"
#include "repository.h"
#include "strbuf.h"
+#include "tempfile.h"
+#include "write-or-die.h"
static int append_loose_object(const struct object_id *oid,
const char *path UNUSED,
@@ -583,6 +586,241 @@ static int odb_source_loose_freshen_object(struct odb_source *source,
return !!check_and_freshen_file(path.buf, 1, mtime);
}
+/* Finalize a file on disk, and close it. */
+static void close_loose_object(struct odb_source_loose *loose,
+ int fd, const char *filename)
+{
+ if (loose->base.will_destroy)
+ goto out;
+
+ if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+ odb_transaction_files_fsync(loose->base.odb->transaction, fd, filename);
+ else if (fsync_object_files > 0)
+ fsync_or_die(fd, filename);
+ else
+ fsync_component_or_die(FSYNC_COMPONENT_LOOSE_OBJECT, fd,
+ filename);
+
+out:
+ if (close(fd) != 0)
+ die_errno(_("error when closing loose object file"));
+}
+
+/* Size of directory component, including the ending '/' */
+static inline int directory_size(const char *filename)
+{
+ const char *s = strrchr(filename, '/');
+ if (!s)
+ return 0;
+ return s - filename + 1;
+}
+
+/*
+ * This creates a temporary file in the same directory as the final
+ * 'filename'
+ *
+ * We want to avoid cross-directory filename renames, because those
+ * can have problems on various filesystems (FAT, NFS, Coda).
+ */
+static int create_tmpfile(struct repository *repo,
+ struct strbuf *tmp, const char *filename)
+{
+ int fd, dirlen = directory_size(filename);
+
+ strbuf_reset(tmp);
+ strbuf_add(tmp, filename, dirlen);
+ strbuf_addstr(tmp, "tmp_obj_XXXXXX");
+ fd = git_mkstemp_mode(tmp->buf, 0444);
+ if (fd < 0 && dirlen && errno == ENOENT) {
+ /*
+ * Make sure the directory exists; note that the contents
+ * of the buffer are undefined after mkstemp returns an
+ * error, so we have to rewrite the whole buffer from
+ * scratch.
+ */
+ strbuf_reset(tmp);
+ strbuf_add(tmp, filename, dirlen - 1);
+ if (mkdir(tmp->buf, 0777) && errno != EEXIST)
+ return -1;
+ if (adjust_shared_perm(repo, tmp->buf))
+ return -1;
+
+ /* Try again */
+ strbuf_addstr(tmp, "/tmp_obj_XXXXXX");
+ fd = git_mkstemp_mode(tmp->buf, 0444);
+ }
+ return fd;
+}
+
+/**
+ * Common steps for loose object writers to start writing loose
+ * objects:
+ *
+ * - Create tmpfile for the loose object.
+ * - Setup zlib stream for compression.
+ * - Start to feed header to zlib stream.
+ *
+ * Returns a "fd", which should later be provided to
+ * end_loose_object_common().
+ */
+static int start_loose_object_common(struct odb_source_loose *loose,
+ struct strbuf *tmp_file,
+ const char *filename, unsigned flags,
+ git_zstream *stream,
+ unsigned char *buf, size_t buflen,
+ struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+ char *hdr, int hdrlen)
+{
+ const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo;
+ const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+ int fd;
+ struct repo_config_values *cfg = repo_config_values(loose->base.odb->repo);
+
+ fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename);
+ if (fd < 0) {
+ if (flags & ODB_WRITE_OBJECT_SILENT)
+ return -1;
+ else if (errno == EACCES)
+ return error(_("insufficient permission for adding "
+ "an object to repository database %s"),
+ loose->base.path);
+ else
+ return error_errno(
+ _("unable to create temporary file"));
+ }
+
+ /* Setup zlib stream for compression */
+ git_deflate_init(stream, cfg->zlib_compression_level);
+ stream->next_out = buf;
+ stream->avail_out = buflen;
+ git_hash_init(c, algo);
+ if (compat && compat_c)
+ git_hash_init(compat_c, compat);
+
+ /* Start to feed header to zlib stream */
+ stream->next_in = (unsigned char *)hdr;
+ stream->avail_in = hdrlen;
+ while (git_deflate(stream, 0) == Z_OK)
+ ; /* nothing */
+ git_hash_update(c, hdr, hdrlen);
+ if (compat && compat_c)
+ git_hash_update(compat_c, hdr, hdrlen);
+
+ return fd;
+}
+
+/**
+ * Common steps for the inner git_deflate() loop for writing loose
+ * objects. Returns what git_deflate() returns.
+ */
+static int write_loose_object_common(struct odb_source_loose *loose,
+ struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+ git_zstream *stream, const int flush,
+ unsigned char *in0, const int fd,
+ unsigned char *compressed,
+ const size_t compressed_len)
+{
+ const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+ int ret;
+
+ ret = git_deflate(stream, flush ? Z_FINISH : 0);
+ git_hash_update(c, in0, stream->next_in - in0);
+ if (compat && compat_c)
+ git_hash_update(compat_c, in0, stream->next_in - in0);
+ if (write_in_full(fd, compressed, stream->next_out - compressed) < 0)
+ die_errno(_("unable to write loose object file"));
+ stream->next_out = compressed;
+ stream->avail_out = compressed_len;
+
+ return ret;
+}
+
+/**
+ * Common steps for loose object writers to end writing loose objects:
+ *
+ * - End the compression of zlib stream.
+ * - Get the calculated oid to "oid".
+ */
+static int end_loose_object_common(struct odb_source_loose *loose,
+ struct git_hash_ctx *c, struct git_hash_ctx *compat_c,
+ git_zstream *stream, struct object_id *oid,
+ struct object_id *compat_oid)
+{
+ const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+ int ret;
+
+ ret = git_deflate_end_gently(stream);
+ if (ret != Z_OK)
+ return ret;
+ git_hash_final_oid(oid, c);
+ if (compat && compat_c)
+ git_hash_final_oid(compat_oid, compat_c);
+
+ return Z_OK;
+}
+
+static int write_loose_object(struct odb_source_loose *loose,
+ const struct object_id *oid, char *hdr,
+ int hdrlen, const void *buf, unsigned long len,
+ const time_t *mtime, unsigned flags)
+{
+ int fd, ret;
+ unsigned char compressed[4096];
+ git_zstream stream;
+ struct git_hash_ctx c;
+ struct object_id parano_oid;
+ static struct strbuf tmp_file = STRBUF_INIT;
+ static struct strbuf filename = STRBUF_INIT;
+
+ if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+ odb_transaction_files_prepare(loose->base.odb->transaction);
+
+ odb_loose_path(loose, &filename, oid);
+
+ fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags,
+ &stream, compressed, sizeof(compressed),
+ &c, NULL, hdr, hdrlen);
+ if (fd < 0)
+ return -1;
+
+ /* Then the data itself.. */
+ stream.next_in = (void *)buf;
+ stream.avail_in = len;
+ do {
+ unsigned char *in0 = stream.next_in;
+
+ ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd,
+ compressed, sizeof(compressed));
+ } while (ret == Z_OK);
+
+ if (ret != Z_STREAM_END)
+ die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid),
+ ret);
+ ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL);
+ if (ret != Z_OK)
+ die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid),
+ ret);
+ if (!oideq(oid, ¶no_oid))
+ die(_("confused by unstable object source data for %s"),
+ oid_to_hex(oid));
+
+ close_loose_object(loose, fd, tmp_file.buf);
+
+ if (mtime) {
+ struct utimbuf utb = {
+ .actime = *mtime,
+ .modtime = *mtime,
+ };
+
+ if (utime(tmp_file.buf, &utb) < 0 &&
+ !(flags & ODB_WRITE_OBJECT_SILENT))
+ warning_errno(_("failed utime() on %s"), tmp_file.buf);
+ }
+
+ return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
+ FOF_SKIP_COLLISION_CHECK);
+}
+
static int odb_source_loose_write_object(struct odb_source *source,
const void *buf, size_t len,
enum object_type type,
@@ -611,12 +849,120 @@ static int odb_source_loose_write_object_stream(struct odb_source *source,
size_t len,
struct object_id *oid)
{
+ struct odb_source_loose *loose = odb_source_loose_downcast(source);
+ const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo;
+ struct object_id compat_oid;
+ int fd, ret, err = 0, flush = 0;
+ unsigned char compressed[4096];
+ git_zstream stream;
+ struct git_hash_ctx c, compat_c;
+ struct strbuf tmp_file = STRBUF_INIT;
+ struct strbuf filename = STRBUF_INIT;
+ unsigned char buf[8192];
+ int dirlen;
+ char hdr[MAX_HEADER_LEN];
+ int hdrlen;
+
+ if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT))
+ odb_transaction_files_prepare(loose->base.odb->transaction);
+
+ /* Since oid is not determined, save tmp file to odb path. */
+ strbuf_addf(&filename, "%s/", loose->base.path);
+ hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len);
+
/*
- * TODO: the implementation should be moved here, see the comment on
- * the called function in "object-file.h".
+ * Common steps for write_loose_object and stream_loose_object to
+ * start writing loose objects:
+ *
+ * - Create tmpfile for the loose object.
+ * - Setup zlib stream for compression.
+ * - Start to feed header to zlib stream.
*/
- struct odb_source_loose *loose = odb_source_loose_downcast(source);
- return odb_source_loose_write_stream(loose, in_stream, len, oid);
+ fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0,
+ &stream, compressed, sizeof(compressed),
+ &c, &compat_c, hdr, hdrlen);
+ if (fd < 0) {
+ err = -1;
+ goto cleanup;
+ }
+
+ /* Then the data itself.. */
+ do {
+ unsigned char *in0 = stream.next_in;
+
+ if (!stream.avail_in && !in_stream->is_finished) {
+ ssize_t read_len = odb_write_stream_read(in_stream, buf,
+ sizeof(buf));
+ if (read_len < 0) {
+ close(fd);
+ err = -1;
+ goto cleanup;
+ }
+
+ stream.avail_in = read_len;
+ stream.next_in = buf;
+ in0 = buf;
+ /* All data has been read. */
+ if (in_stream->is_finished)
+ flush = 1;
+ }
+ ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd,
+ compressed, sizeof(compressed));
+ /*
+ * Unlike write_loose_object(), we do not have the entire
+ * buffer. If we get Z_BUF_ERROR due to too few input bytes,
+ * then we'll replenish them in the next input_stream->read()
+ * call when we loop.
+ */
+ } while (ret == Z_OK || ret == Z_BUF_ERROR);
+
+ if (stream.total_in != len + hdrlen)
+ die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in,
+ (uintmax_t)len + hdrlen);
+
+ /*
+ * Common steps for write_loose_object and stream_loose_object to
+ * end writing loose object:
+ *
+ * - End the compression of zlib stream.
+ * - Get the calculated oid.
+ */
+ if (ret != Z_STREAM_END)
+ die(_("unable to stream deflate new object (%d)"), ret);
+ ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid);
+ if (ret != Z_OK)
+ die(_("deflateEnd on stream object failed (%d)"), ret);
+ close_loose_object(loose, fd, tmp_file.buf);
+
+ if (odb_freshen_object(loose->base.odb, oid)) {
+ unlink_or_warn(tmp_file.buf);
+ goto cleanup;
+ }
+ odb_loose_path(loose, &filename, oid);
+
+ /* We finally know the object path, and create the missing dir. */
+ dirlen = directory_size(filename.buf);
+ if (dirlen) {
+ struct strbuf dir = STRBUF_INIT;
+ strbuf_add(&dir, filename.buf, dirlen);
+
+ if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) &&
+ errno != EEXIST) {
+ err = error_errno(_("unable to create directory %s"), dir.buf);
+ strbuf_release(&dir);
+ goto cleanup;
+ }
+ strbuf_release(&dir);
+ }
+
+ err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf,
+ FOF_SKIP_COLLISION_CHECK);
+ if (!err && compat)
+ err = repo_add_loose_object_map(loose, oid, &compat_oid);
+cleanup:
+ strbuf_release(&tmp_file);
+ strbuf_release(&filename);
+ return err;
}
static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED,
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 8/9] object-file: move `force_object_loose()`
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
In the preceding commits we have refactored `force_object_loose()` to
not call internal functions anymore for writing the object. Instead, it
now only uses generic functions that are accessible to all callers.
Consequently, we can now easily move the function to its only caller,
which is git-pack-objects(1). Do so.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/pack-objects.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++
object-file.c | 44 --------------------------------------------
object-file.h | 4 ----
3 files changed, 46 insertions(+), 48 deletions(-)
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index e64a96f1a7..bb3bc486e8 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -32,6 +32,7 @@
#include "list.h"
#include "packfile.h"
#include "object-file.h"
+#include "object-file-convert.h"
#include "odb.h"
#include "odb/streaming.h"
#include "replace-object.h"
@@ -4622,6 +4623,51 @@ static int loosened_object_can_be_discarded(const struct object_id *oid,
return 1;
}
+static int force_object_loose(struct odb_source *source,
+ const struct object_id *oid,
+ const time_t *mtime)
+{
+ struct odb_source_files *files = odb_source_files_downcast(source);
+ const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
+ struct object_info oi = OBJECT_INFO_INIT;
+ struct object_id compat_oid, *compat_oid_p = NULL;
+ enum object_type type;
+ void *buf = NULL;
+ size_t len;
+ int ret;
+
+ for (struct odb_source *s = source->odb->sources; s; s = s->next) {
+ struct odb_source_files *files = odb_source_files_downcast(s);
+ if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
+ return 0;
+ }
+
+ oi.typep = &type;
+ oi.sizep = &len;
+ oi.contentp = &buf;
+ if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {
+ ret = error(_("cannot read object for %s"), oid_to_hex(oid));
+ goto out;
+ }
+
+ if (compat) {
+ if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {
+ ret = error(_("cannot map object %s to %s"),
+ oid_to_hex(oid), compat->name);
+ goto out;
+ }
+
+ compat_oid_p = &compat_oid;
+ }
+
+ ret = odb_source_write_object(&files->loose->base, buf, len, type, oid,
+ compat_oid_p, mtime, 0);
+
+out:
+ free(buf);
+ return ret;
+}
+
static void loosen_unused_packed_objects(void)
{
struct packed_git *p;
diff --git a/object-file.c b/object-file.c
index 89825feed0..b867d8d9de 100644
--- a/object-file.c
+++ b/object-file.c
@@ -893,50 +893,6 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,
return err;
}
-int force_object_loose(struct odb_source *source,
- const struct object_id *oid, const time_t *mtime)
-{
- struct odb_source_files *files = odb_source_files_downcast(source);
- const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
- struct object_info oi = OBJECT_INFO_INIT;
- struct object_id compat_oid, *compat_oid_p = NULL;
- enum object_type type;
- void *buf = NULL;
- size_t len;
- int ret;
-
- for (struct odb_source *s = source->odb->sources; s; s = s->next) {
- struct odb_source_files *files = odb_source_files_downcast(s);
- if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0))
- return 0;
- }
-
- oi.typep = &type;
- oi.sizep = &len;
- oi.contentp = &buf;
- if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {
- ret = error(_("cannot read object for %s"), oid_to_hex(oid));
- goto out;
- }
-
- if (compat) {
- if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {
- ret = error(_("cannot map object %s to %s"),
- oid_to_hex(oid), compat->name);
- goto out;
- }
-
- compat_oid_p = &compat_oid;
- }
-
- ret = odb_source_write_object(&files->loose->base, buf, len, type, oid,
- compat_oid_p, mtime, 0);
-
-out:
- free(buf);
- return ret;
-}
-
/*
* We can't use the normal fsck_error_function() for index_mem(),
* because we don't yet have a valid oid for it to report. Instead,
diff --git a/object-file.h b/object-file.h
index 9fd540afb6..31781a9c53 100644
--- a/object-file.h
+++ b/object-file.h
@@ -98,10 +98,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr,
int format_object_header(char *str, size_t size, enum object_type type,
size_t objsize);
-int force_object_loose(struct odb_source *source,
- const struct object_id *oid,
- const time_t *mtime);
-
/**
* With in-core object data in "buf", rehash it to make sure the
* object name actually matches "oid" to detect object corruption.
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 7/9] object-file: force objects loose via generic interface
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
When repacking objects we may end up "loosening" objects via
`force_objects_loose()`. The implementation of this logic still sits
with "object-file.c" even though it is ultimately an implementation
detail of the "files" backend.
Moving this logic around is non-trivial though as we depend on
`write_loose_object()`, which is an internal implementation detail of
how we write loose objects. Until now it wasn't possible to use the
generic function `odb_source_write_object()` though, because the "loose"
implementation thereof would skip writing the object in case it already
exists in any other source.
This restriction was lifted over the preceding commits though, where
this object existence check is now handled on the object database level
and not on the individual source level anymore. Consequently, it is now
possible to use generic interfaces.
Refactor the code accordingly so that we can move the logic around in a
subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
object-file.c | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/object-file.c b/object-file.c
index 067a63a4f1..89825feed0 100644
--- a/object-file.c
+++ b/object-file.c
@@ -898,13 +898,11 @@ int force_object_loose(struct odb_source *source,
{
struct odb_source_files *files = odb_source_files_downcast(source);
const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
- void *buf = NULL;
- size_t len;
struct object_info oi = OBJECT_INFO_INIT;
- struct object_id compat_oid;
+ struct object_id compat_oid, *compat_oid_p = NULL;
enum object_type type;
- char hdr[MAX_HEADER_LEN];
- int hdrlen;
+ void *buf = NULL;
+ size_t len;
int ret;
for (struct odb_source *s = source->odb->sources; s; s = s->next) {
@@ -927,15 +925,12 @@ int force_object_loose(struct odb_source *source,
oid_to_hex(oid), compat->name);
goto out;
}
- }
- hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
- ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);
- if (ret)
- goto out;
+ compat_oid_p = &compat_oid;
+ }
- if (compat)
- ret = repo_add_loose_object_map(files->loose, oid, &compat_oid);
+ ret = odb_source_write_object(&files->loose->base, buf, len, type, oid,
+ compat_oid_p, mtime, 0);
out:
free(buf);
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 6/9] object-file: fix memory leak in `force_object_loose()`
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
We return an error when converting the given object to the compatibility
hash algorithm fails. This early return causes a memory leak though,
because we don't free the content buffer that we've already read before
via `odb_read_object_info_extended()`.
Plug the memory leak by creating a common exit path where the buffer
gets free'd.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
object-file.c | 26 ++++++++++++++++++--------
1 file changed, 18 insertions(+), 8 deletions(-)
diff --git a/object-file.c b/object-file.c
index 5b07530950..067a63a4f1 100644
--- a/object-file.c
+++ b/object-file.c
@@ -898,7 +898,7 @@ int force_object_loose(struct odb_source *source,
{
struct odb_source_files *files = odb_source_files_downcast(source);
const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
- void *buf;
+ void *buf = NULL;
size_t len;
struct object_info oi = OBJECT_INFO_INIT;
struct object_id compat_oid;
@@ -916,19 +916,29 @@ int force_object_loose(struct odb_source *source,
oi.typep = &type;
oi.sizep = &len;
oi.contentp = &buf;
- if (odb_read_object_info_extended(source->odb, oid, &oi, 0))
- return error(_("cannot read object for %s"), oid_to_hex(oid));
+ if (odb_read_object_info_extended(source->odb, oid, &oi, 0)) {
+ ret = error(_("cannot read object for %s"), oid_to_hex(oid));
+ goto out;
+ }
+
if (compat) {
- if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid))
- return error(_("cannot map object %s to %s"),
- oid_to_hex(oid), compat->name);
+ if (repo_oid_to_algop(source->odb->repo, oid, compat, &compat_oid)) {
+ ret = error(_("cannot map object %s to %s"),
+ oid_to_hex(oid), compat->name);
+ goto out;
+ }
}
+
hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0);
- if (!ret && compat)
+ if (ret)
+ goto out;
+
+ if (compat)
ret = repo_add_loose_object_map(files->loose, oid, &compat_oid);
- free(buf);
+out:
+ free(buf);
return ret;
}
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 5/9] odb: support setting mtime when writing objects
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
The function `force_object_loose()` is used to loosen packed objects
before repacking. It passes the pack's mtime along so that the newly
written loose object inherits the same timestamp. This matters for
object pruning, which uses the mtime to determine whether an object is
old enough to be pruned.
In a subsequent commit, `force_object_loose()` will be converted to use
the generic `odb_source_write_object()` interface instead of calling
`write_loose_object()` directly. But the generic interface doesn't yet
support setting a specific mtime, which makes it impossible to implement
the logic as of now.
Prepare for the change by introducing a new `mtime` parameter to this
function that we plumb through the stack. If set, the backends are
instructed to set the object's mtime accordingly. If unset, the backends
are expected to use the current time instead.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
builtin/pack-objects.c | 2 +-
object-file.c | 29 ++++++++++++++++++++---------
object-file.h | 8 +++++---
odb.c | 6 +++---
odb/source-files.c | 10 ++++++----
odb/source-inmemory.c | 6 ++++--
odb/source-loose.c | 8 +++++---
odb/source-packed.c | 13 +++++++++++--
odb/source.h | 12 ++++++++----
read-cache.c | 2 +-
t/unit-tests/u-odb-inmemory.c | 6 +++---
11 files changed, 67 insertions(+), 35 deletions(-)
diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c
index ea5eab4cf8..e64a96f1a7 100644
--- a/builtin/pack-objects.c
+++ b/builtin/pack-objects.c
@@ -4642,7 +4642,7 @@ static void loosen_unused_packed_objects(void)
!has_sha1_pack_kept_or_nonlocal(&oid) &&
!loosened_object_can_be_discarded(&oid, p->mtime)) {
if (force_object_loose(the_repository->objects->sources,
- &oid, p->mtime))
+ &oid, &p->mtime))
die(_("unable to force loose object"));
loosened_objects_nr++;
}
diff --git a/object-file.c b/object-file.c
index 9ca14f484d..5b07530950 100644
--- a/object-file.c
+++ b/object-file.c
@@ -67,9 +67,17 @@ const char *odb_loose_path(struct odb_source_loose *loose,
}
/* Returns 1 if we have successfully freshened the file, 0 otherwise. */
-static int freshen_file(const char *fn)
+static int freshen_file(const char *fn, const time_t *mtime)
{
- return !utime(fn, NULL);
+ struct utimbuf times, *timesp = NULL;
+
+ if (mtime) {
+ times.actime = *mtime;
+ times.modtime = *mtime;
+ timesp = ×
+ }
+
+ return !utime(fn, timesp);
}
/*
@@ -79,11 +87,12 @@ static int freshen_file(const char *fn)
* either does not exist on disk, or has a stale mtime and may be subject to
* pruning).
*/
-int check_and_freshen_file(const char *fn, int freshen)
+int check_and_freshen_file(const char *fn, int freshen,
+ const time_t *mtime)
{
if (access(fn, F_OK))
return 0;
- if (freshen && !freshen_file(fn))
+ if (freshen && !freshen_file(fn, mtime))
return 0;
return 1;
}
@@ -706,7 +715,7 @@ static int end_loose_object_common(struct odb_source_loose *loose,
int write_loose_object(struct odb_source_loose *loose,
const struct object_id *oid, char *hdr,
int hdrlen, const void *buf, unsigned long len,
- time_t mtime, unsigned flags)
+ const time_t *mtime, unsigned flags)
{
int fd, ret;
unsigned char compressed[4096];
@@ -751,9 +760,11 @@ int write_loose_object(struct odb_source_loose *loose,
close_loose_object(loose, fd, tmp_file.buf);
if (mtime) {
- struct utimbuf utb;
- utb.actime = mtime;
- utb.modtime = mtime;
+ struct utimbuf utb = {
+ .actime = *mtime,
+ .modtime = *mtime,
+ };
+
if (utime(tmp_file.buf, &utb) < 0 &&
!(flags & ODB_WRITE_OBJECT_SILENT))
warning_errno(_("failed utime() on %s"), tmp_file.buf);
@@ -883,7 +894,7 @@ int odb_source_loose_write_stream(struct odb_source_loose *loose,
}
int force_object_loose(struct odb_source *source,
- const struct object_id *oid, time_t mtime)
+ const struct object_id *oid, const time_t *mtime)
{
struct odb_source_files *files = odb_source_files_downcast(source);
const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
diff --git a/object-file.h b/object-file.h
index 08aafcda0d..9fd540afb6 100644
--- a/object-file.h
+++ b/object-file.h
@@ -99,7 +99,8 @@ int format_object_header(char *str, size_t size, enum object_type type,
size_t objsize);
int force_object_loose(struct odb_source *source,
- const struct object_id *oid, time_t mtime);
+ const struct object_id *oid,
+ const time_t *mtime);
/**
* With in-core object data in "buf", rehash it to make sure the
@@ -137,10 +138,11 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf,
int write_loose_object(struct odb_source_loose *loose,
const struct object_id *oid, char *hdr,
int hdrlen, const void *buf, unsigned long len,
- time_t mtime, unsigned flags);
+ const time_t *mtime, unsigned flags);
/* Helper to check and "touch" a file */
-int check_and_freshen_file(const char *fn, int freshen);
+int check_and_freshen_file(const char *fn, int freshen,
+ const time_t *mtime);
/*
* Open the loose object at path, check its hash, and return the contents,
diff --git a/odb.c b/odb.c
index bfeca76f4e..dabd481f57 100644
--- a/odb.c
+++ b/odb.c
@@ -738,7 +738,7 @@ int odb_pretend_object(struct object_database *odb,
return 0;
return odb_source_write_object(odb->inmemory_objects,
- buf, len, type, oid, NULL, 0);
+ buf, len, type, oid, NULL, NULL, 0);
}
void *odb_read_object(struct object_database *odb,
@@ -829,7 +829,7 @@ int odb_freshen_object(struct object_database *odb,
struct odb_source *source;
odb_prepare_alternates(odb);
for (source = odb->sources; source; source = source->next)
- if (odb_source_freshen_object(source, oid))
+ if (odb_source_freshen_object(source, oid, NULL))
return 1;
return 0;
}
@@ -1024,7 +1024,7 @@ int odb_write_object_ext(struct object_database *odb,
}
return odb_source_write_object(odb->sources, buf, len, type,
- oid, compat_oid_p, flags);
+ oid, compat_oid_p, NULL, flags);
}
int odb_write_object_stream(struct object_database *odb,
diff --git a/odb/source-files.c b/odb/source-files.c
index 06dfc8dd78..4df4e1af6c 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -150,11 +150,12 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source,
}
static int odb_source_files_freshen_object(struct odb_source *source,
- const struct object_id *oid)
+ const struct object_id *oid,
+ const time_t *mtime)
{
struct odb_source_files *files = odb_source_files_downcast(source);
- if (odb_source_freshen_object(&files->packed->base, oid) ||
- odb_source_freshen_object(&files->loose->base, oid))
+ if (odb_source_freshen_object(&files->packed->base, oid, mtime) ||
+ odb_source_freshen_object(&files->loose->base, oid, mtime))
return 1;
return 0;
}
@@ -164,11 +165,12 @@ static int odb_source_files_write_object(struct odb_source *source,
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid,
+ const time_t *mtime,
enum odb_write_object_flags flags)
{
struct odb_source_files *files = odb_source_files_downcast(source);
return odb_source_write_object(&files->loose->base, buf, len, type,
- oid, compat_oid, flags);
+ oid, compat_oid, mtime, flags);
}
static int odb_source_files_write_object_stream(struct odb_source *source,
diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
index 963d520317..3e71611b8e 100644
--- a/odb/source-inmemory.c
+++ b/odb/source-inmemory.c
@@ -232,6 +232,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source,
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid UNUSED,
+ const time_t *mtime UNUSED,
enum odb_write_object_flags flags UNUSED)
{
struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
@@ -286,7 +287,7 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,
hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);
ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,
- NULL, 0);
+ NULL, NULL, 0);
if (ret < 0)
goto out;
@@ -296,7 +297,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,
}
static int odb_source_inmemory_freshen_object(struct odb_source *source,
- const struct object_id *oid)
+ const struct object_id *oid,
+ const time_t *mtime UNUSED)
{
struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
if (find_cached_object(inmemory, oid))
diff --git a/odb/source-loose.c b/odb/source-loose.c
index 04af1a54a3..520a30157c 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -574,12 +574,13 @@ static int odb_source_loose_count_objects(struct odb_source *source,
}
static int odb_source_loose_freshen_object(struct odb_source *source,
- const struct object_id *oid)
+ const struct object_id *oid,
+ const time_t *mtime)
{
struct odb_source_loose *loose = odb_source_loose_downcast(source);
static struct strbuf path = STRBUF_INIT;
odb_loose_path(loose, &path, oid);
- return !!check_and_freshen_file(path.buf, 1);
+ return !!check_and_freshen_file(path.buf, 1, mtime);
}
static int odb_source_loose_write_object(struct odb_source *source,
@@ -587,6 +588,7 @@ static int odb_source_loose_write_object(struct odb_source *source,
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid,
+ const time_t *mtime,
enum odb_write_object_flags flags)
{
struct odb_source_loose *loose = odb_source_loose_downcast(source);
@@ -595,7 +597,7 @@ static int odb_source_loose_write_object(struct odb_source *source,
hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
- if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
+ if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, mtime, flags))
return -1;
if (compat_oid)
diff --git a/odb/source-packed.c b/odb/source-packed.c
index f7f1706447..5e5da9bc54 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -507,18 +507,26 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,
}
static int odb_source_packed_freshen_object(struct odb_source *source,
- const struct object_id *oid)
+ const struct object_id *oid,
+ const time_t *mtime)
{
struct odb_source_packed *packed = odb_source_packed_downcast(source);
+ struct utimbuf times, *timesp = NULL;
struct pack_entry e;
+ if (mtime) {
+ times.actime = *mtime;
+ times.modtime = *mtime;
+ timesp = ×
+ }
+
if (!find_pack_entry(packed, oid, &e))
return 0;
if (e.p->is_cruft)
return 0;
if (e.p->freshened)
return 1;
- if (utime(e.p->pack_name, NULL))
+ if (utime(e.p->pack_name, timesp))
return 0;
e.p->freshened = 1;
@@ -531,6 +539,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,
enum object_type type UNUSED,
const struct object_id *oid UNUSED,
const struct object_id *compat_oid UNUSED,
+ const time_t *mtime UNUSED,
unsigned flags UNUSED)
{
return error("packed backend cannot write objects");
diff --git a/odb/source.h b/odb/source.h
index c4e94c9d0d..fc04dd5cda 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -190,7 +190,8 @@ struct odb_source {
* has been freshened.
*/
int (*freshen_object)(struct odb_source *source,
- const struct object_id *oid);
+ const struct object_id *oid,
+ const time_t *mtime);
/*
* This callback is expected to persist the given object into the
@@ -208,6 +209,7 @@ struct odb_source {
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid,
+ const time_t *mtime,
enum odb_write_object_flags flags);
/*
@@ -403,9 +405,10 @@ static inline int odb_source_find_abbrev_len(struct odb_source *source,
* not exist.
*/
static inline int odb_source_freshen_object(struct odb_source *source,
- const struct object_id *oid)
+ const struct object_id *oid,
+ const time_t *mtime)
{
- return source->freshen_object(source, oid);
+ return source->freshen_object(source, oid, mtime);
}
/*
@@ -418,10 +421,11 @@ static inline int odb_source_write_object(struct odb_source *source,
enum object_type type,
const struct object_id *oid,
const struct object_id *compat_oid,
+ const time_t *mtime,
enum odb_write_object_flags flags)
{
return source->write_object(source, buf, len, type, oid,
- compat_oid, flags);
+ compat_oid, mtime, flags);
}
/*
diff --git a/read-cache.c b/read-cache.c
index 3510b49edf..c67930177f 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -2342,7 +2342,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist)
*/
static void freshen_shared_index(const char *shared_index, int warn)
{
- if (!check_and_freshen_file(shared_index, 1) && warn)
+ if (!check_and_freshen_file(shared_index, 1, NULL) && warn)
warning(_("could not freshen shared index '%s'"), shared_index);
}
diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c
index 28a69fc244..ddf2db5c81 100644
--- a/t/unit-tests/u-odb-inmemory.c
+++ b/t/unit-tests/u-odb-inmemory.c
@@ -45,7 +45,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source,
size_t content_len = strlen(content);
hash_object_file(repo.hash_algo, content, content_len, type, oid);
cl_must_pass(odb_source_write_object(&source->base, content, content_len,
- type, oid, NULL, 0));
+ type, oid, NULL, NULL, 0));
}
void test_odb_inmemory__initialize(void)
@@ -256,11 +256,11 @@ void test_odb_inmemory__freshen_object(void)
const char *end;
cl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo));
- cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0);
+ cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid, NULL), 0);
cl_assert_write_object(source, "foobar", OBJ_BLOB, &written_oid);
cl_assert_equal_i(odb_source_freshen_object(&source->base,
- &written_oid), 1);
+ &written_oid, NULL), 1);
odb_source_free(&source->base);
}
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 4/9] odb: lift object existence check out of the "loose" backend
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
Before writing a new loose object we first check whether the object
already exists in any of the sources attached to the object database.
This results in a couple of issues:
- We have a layering violation, where the source needs to be aware of
objects stored in any of the other sources.
- Every backend would have to reimplement this check, which feels
somewhat pointless.
- It is not possible to easily write an object into a source in case
the same object already exists in another source.
Refactor the code and lift up the object existence check from the
"loose" backend into the generic ODB layer. No callers need adjustment
as none of them write via a specific source, but via the ODB layer.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
odb.c | 7 +++++++
odb/source-loose.c | 8 ++------
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/odb.c b/odb.c
index 4adbdf8a64..bfeca76f4e 100644
--- a/odb.c
+++ b/odb.c
@@ -997,6 +997,13 @@ int odb_write_object_ext(struct object_database *odb,
hash_object_file(odb->repo->hash_algo, buf, len, type, oid);
+ /*
+ * We can skip the write in case we already have the object available.
+ * In that case, we only freshen its mtime.
+ */
+ if (odb_freshen_object(odb, oid))
+ return 0;
+
if (compat) {
const struct git_hash_algo *algo = odb->repo->hash_algo;
diff --git a/odb/source-loose.c b/odb/source-loose.c
index d4715da6d1..04af1a54a3 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -595,16 +595,12 @@ static int odb_source_loose_write_object(struct odb_source *source,
hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
- /*
- * Normally if we have it in the pack then we do not bother writing
- * it out into .git/objects/??/?{38} file.
- */
- if (odb_freshen_object(source->odb, oid))
- return 0;
if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
return -1;
+
if (compat_oid)
return repo_add_loose_object_map(loose, oid, compat_oid);
+
return 0;
}
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
* [PATCH 3/9] odb: compute object hash in `odb_write_object_ext()`
From: Patrick Steinhardt @ 2026-07-17 9:32 UTC (permalink / raw)
To: git; +Cc: Justin Tobler
In-Reply-To: <20260717-pks-odb-move-loose-object-writing-v1-0-46446a3cb5b7@pks.im>
Same as in a preceding commit, compute the object hash in
`odb_write_object_ext()` so that we can unify this logic.
Besides unification, this change also allows us to lift the object
existence check out of the "loose" backend into the generic layer, which
will happen in the next commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
object-file.c | 35 ++++++++---------------------------
object-file.h | 4 ----
odb.c | 2 ++
odb/source-files.c | 2 +-
odb/source-inmemory.c | 6 +++---
odb/source-loose.c | 12 +++++++-----
odb/source-packed.c | 2 +-
odb/source.h | 4 ++--
t/unit-tests/u-odb-inmemory.c | 1 +
9 files changed, 25 insertions(+), 43 deletions(-)
diff --git a/object-file.c b/object-file.c
index 5283292f1e..9ca14f484d 100644
--- a/object-file.c
+++ b/object-file.c
@@ -316,31 +316,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi)
return 0;
}
-static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c,
- const void *buf, size_t len,
- struct object_id *oid,
- char *hdr, size_t *hdrlen)
-{
- git_hash_init(c, algo);
- git_hash_update(c, hdr, *hdrlen);
- git_hash_update(c, buf, len);
- git_hash_final_oid(oid, c);
-}
-
-void write_object_file_prepare(const struct git_hash_algo *algo,
- const void *buf, size_t len,
- enum object_type type, struct object_id *oid,
- char *hdr, size_t *hdrlen)
-{
- struct git_hash_ctx c;
-
- /* Generate the header */
- *hdrlen = format_object_header(hdr, *hdrlen, type, len);
-
- /* Hash (function pointers) computation */
- hash_object_body(algo, &c, buf, len, oid, hdr, hdrlen);
-}
-
#define CHECK_COLLISION_DEST_VANISHED -2
static int check_collision(const char *source, const char *dest)
@@ -476,10 +451,16 @@ void hash_object_file(const struct git_hash_algo *algo, const void *buf,
size_t len, enum object_type type,
struct object_id *oid)
{
+ struct git_hash_ctx c;
char hdr[MAX_HEADER_LEN];
- size_t hdrlen = sizeof(hdr);
+ int hdrlen;
+
+ hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
- write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
+ git_hash_init(&c, algo);
+ git_hash_update(&c, hdr, hdrlen);
+ git_hash_update(&c, buf, len);
+ git_hash_final_oid(oid, &c);
}
struct transaction_packfile {
diff --git a/object-file.h b/object-file.h
index d04ffa6493..08aafcda0d 100644
--- a/object-file.h
+++ b/object-file.h
@@ -134,10 +134,6 @@ int finalize_object_file_flags(struct repository *repo,
void hash_object_file(const struct git_hash_algo *algo, const void *buf,
size_t len, enum object_type type,
struct object_id *oid);
-void write_object_file_prepare(const struct git_hash_algo *algo,
- const void *buf, size_t len,
- enum object_type type, struct object_id *oid,
- char *hdr, size_t *hdrlen);
int write_loose_object(struct odb_source_loose *loose,
const struct object_id *oid, char *hdr,
int hdrlen, const void *buf, unsigned long len,
diff --git a/odb.c b/odb.c
index 1d6538163b..4adbdf8a64 100644
--- a/odb.c
+++ b/odb.c
@@ -995,6 +995,8 @@ int odb_write_object_ext(struct object_database *odb,
const struct git_hash_algo *compat = odb->repo->compat_hash_algo;
struct object_id compat_oid, *compat_oid_p = NULL;
+ hash_object_file(odb->repo->hash_algo, buf, len, type, oid);
+
if (compat) {
const struct git_hash_algo *algo = odb->repo->hash_algo;
diff --git a/odb/source-files.c b/odb/source-files.c
index 3d9f5eca32..06dfc8dd78 100644
--- a/odb/source-files.c
+++ b/odb/source-files.c
@@ -162,7 +162,7 @@ static int odb_source_files_freshen_object(struct odb_source *source,
static int odb_source_files_write_object(struct odb_source *source,
const void *buf, size_t len,
enum object_type type,
- struct object_id *oid,
+ const struct object_id *oid,
const struct object_id *compat_oid,
enum odb_write_object_flags flags)
{
diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c
index e727aba427..963d520317 100644
--- a/odb/source-inmemory.c
+++ b/odb/source-inmemory.c
@@ -230,15 +230,13 @@ static int odb_source_inmemory_count_objects(struct odb_source *source,
static int odb_source_inmemory_write_object(struct odb_source *source,
const void *buf, size_t len,
enum object_type type,
- struct object_id *oid,
+ const struct object_id *oid,
const struct object_id *compat_oid UNUSED,
enum odb_write_object_flags flags UNUSED)
{
struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
struct inmemory_object *object;
- hash_object_file(source->odb->repo->hash_algo, buf, len, type, oid);
-
if (!inmemory->objects) {
CALLOC_ARRAY(inmemory->objects, 1);
oidtree_init(inmemory->objects);
@@ -285,6 +283,8 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source,
goto out;
}
+ hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);
+
ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,
NULL, 0);
if (ret < 0)
diff --git a/odb/source-loose.c b/odb/source-loose.c
index ca223109cd..d4715da6d1 100644
--- a/odb/source-loose.c
+++ b/odb/source-loose.c
@@ -584,19 +584,21 @@ static int odb_source_loose_freshen_object(struct odb_source *source,
static int odb_source_loose_write_object(struct odb_source *source,
const void *buf, size_t len,
- enum object_type type, struct object_id *oid,
+ enum object_type type,
+ const struct object_id *oid,
const struct object_id *compat_oid,
enum odb_write_object_flags flags)
{
struct odb_source_loose *loose = odb_source_loose_downcast(source);
- const struct git_hash_algo *algo = source->odb->repo->hash_algo;
char hdr[MAX_HEADER_LEN];
- size_t hdrlen = sizeof(hdr);
+ int hdrlen;
+
+ hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
- /* Normally if we have it in the pack then we do not bother writing
+ /*
+ * Normally if we have it in the pack then we do not bother writing
* it out into .git/objects/??/?{38} file.
*/
- write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen);
if (odb_freshen_object(source->odb, oid))
return 0;
if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
diff --git a/odb/source-packed.c b/odb/source-packed.c
index af0d533375..f7f1706447 100644
--- a/odb/source-packed.c
+++ b/odb/source-packed.c
@@ -529,7 +529,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,
const void *buf UNUSED,
size_t len UNUSED,
enum object_type type UNUSED,
- struct object_id *oid UNUSED,
+ const struct object_id *oid UNUSED,
const struct object_id *compat_oid UNUSED,
unsigned flags UNUSED)
{
diff --git a/odb/source.h b/odb/source.h
index b3c1ca3a66..c4e94c9d0d 100644
--- a/odb/source.h
+++ b/odb/source.h
@@ -206,7 +206,7 @@ struct odb_source {
int (*write_object)(struct odb_source *source,
const void *buf, size_t len,
enum object_type type,
- struct object_id *oid,
+ const struct object_id *oid,
const struct object_id *compat_oid,
enum odb_write_object_flags flags);
@@ -416,7 +416,7 @@ static inline int odb_source_freshen_object(struct odb_source *source,
static inline int odb_source_write_object(struct odb_source *source,
const void *buf, unsigned long len,
enum object_type type,
- struct object_id *oid,
+ const struct object_id *oid,
const struct object_id *compat_oid,
enum odb_write_object_flags flags)
{
diff --git a/t/unit-tests/u-odb-inmemory.c b/t/unit-tests/u-odb-inmemory.c
index 2dbc3ab1df..28a69fc244 100644
--- a/t/unit-tests/u-odb-inmemory.c
+++ b/t/unit-tests/u-odb-inmemory.c
@@ -43,6 +43,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source,
struct object_id *oid)
{
size_t content_len = strlen(content);
+ hash_object_file(repo.hash_algo, content, content_len, type, oid);
cl_must_pass(odb_source_write_object(&source->base, content, content_len,
type, oid, NULL, 0));
}
--
2.55.0.407.g700c83d4f3.dirty
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox