git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 17:00 UTC

Re: [PATCH] packed-refs: use `fwrite()` when passing refs verbatim

From
Karthik Nayak <karthik.188@gmail.com>
Date
Oct 2, 2026, 00:16 UTC
Message-ID
<CAOLa=ZRTtE+FMrVZBwSmZ-aVBXFphnQB414-PASCAAXJJCgNww@mail.gmail.com>
In-Reply-To
<87y0ch5fkv.fsf@dev.null.iotcl.com.invalid>
Toon Claes <toon@iotcl.com> writes:
Show 8 quoted lines
> Karthik Nayak <karthik.188@gmail.com> writes:
>
>> The `write_with_updates()` function uses a `struct ref_iterator` to
>> iterate over all refs to write to the temporary packfile. It receives
>
> I would say in general "packfile" is only used for objects, but it seems
> there is one mention of "packfile" in this file, so I'm not sure.
>
You're right, I will change both of those to say 'packed-refs'.
Show 46 quoted lines
>> the iterator from `packed_ref_iterator_begin()` which takes a snapshot
>> of the 'packed-refs' file.
>>
>> While writing to the new packfile, writes are routed via
>> `write_packed_entry()` which uses `fprintf()`. Even for references which
>> haven't changed, we use the same mechanism. Instead, let's track the
>> position of unchanged references in the snapshot iterator and directly
>> use `fwrite()`.
>
> There are a few sanitizations that get lost by using fwrite():
>
> * oid_to_hex() is no longer called, so if an OID would be written in the
>   old packed-ref as uppercase, it would be copied as such
>
> * next_record() uses isspace(3) to split the oid from the refname. If
>   the record would be separated by a tab instead of a space, that would
>   be copied over.
>
> * next_record() calls check_refname_format(), if it ain't good, oidclr()
>   is called. Before this patch, that means the zero OID will be written
>   for such ref. That changes with this patch, and the original OID is
>   written if refname_is_safe() passes.
>   This won't make a difference in Git though, because upon reading back
>   from the packed-refs, the zero OID is read into memory anyway, so
>   there is no observable difference.
>
> * similar to the item above, when the ref is peelable, and REF_ISBROKEN
>   (around line 985), the `peeled_oid` is not filled in. This means for a
>   refname not matching the format will not contain a peeled OID for that
>   ref.
>   For example, in the source file:
>
>     1937b04ead6e53d949a4e2eb97ed743b42692fde refs/tags/a-bad~tag
>     ^67c1263aae5c1750e9d9211995e6d1bb38314fca
>
>   is converted to:
>
>     0000000000000000000000000000000000000000 refs/tags/a-bad~tag
>
>   After this patch, the ref and it's peeled commit OID is copied
>   verbatim.
>
> I'm not saying any of this is bad. These are just some side-effects of
> your fwrite() approach. And some might be worht mentioning in the commit
> message.
>

While you're right, sanitation is not part of this flow, here we're simply deleting a reference from the packed-refs file, the fact that sanitation was even happening was a side-effect.

But I will mention it in the commit message, I think that makes sense.
[snip]
Show 23 quoted lines
>>
>> Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
>> ---
>>  refs/packed-backend.c | 43 ++++++++++++++++++++++++++++++++-----------
>>  1 file changed, 32 insertions(+), 11 deletions(-)
>>
>> diff --git a/refs/packed-backend.c b/refs/packed-backend.c
>> index a73fc6aca7..ef952cdba6 100644
>> --- a/refs/packed-backend.c
>> +++ b/refs/packed-backend.c
>> @@ -879,6 +879,12 @@ struct packed_ref_iterator {
>>  	/* The current position in the snapshot's buffer: */
>>  	const char *pos;
>>
>> +	/*
>> +	 * Start of the current record, set when advancing `pos`. Used to
>> +	 * pass records verbatim to `fwrite()`.
>
> I assume there was a version you've been working on that was calling
> fwrite() directly from ref_transaction_error write_with_updates()? With
> that being wrapped by a helper function, I think it's better to name
> that function here.
>
I'm not sure I follow what you mean here.
Show 64 quoted lines
>> +	 */
>> +	const char *record_start;
>> +
>>  	/* The end of the part of the buffer that will be iterated over: */
>>  	const char *eof;
>>
>> @@ -933,6 +939,7 @@ static int next_record(struct packed_ref_iterator *iter)
>>  	if (iter->pos == iter->eof)
>>  		return ITER_DONE;
>>
>> +	iter->record_start = iter->pos;
>>  	iter->base.ref.flags = REF_ISPACKED;
>>  	p = iter->pos;
>>
>> @@ -1218,17 +1225,27 @@ static struct ref_iterator *packed_ref_iterator_begin(
>>
>>  /*
>>   * Write an entry to the packed-refs file for the specified refname.
>> - * If peeled is non-NULL, write it as the entry's peeled value. On
>> - * error, return a nonzero value and leave errno set at the value left
>> - * by the failing call to `fprintf()`.
>> + *
>> + * If the raw data is available, skip the formatting and directly write to
>> + * the file using `fwrite()`. e.g. when deleting references and remaining
>> + * refs need to be written verbatim. Otherwise, use `fprintf()`.
>> + *
>> + * If peeled is non-NULL, write it as the entry's peeled value.
>> + *
>> + * On error, return a nonzero value and leave errno set at the value left
>> + * by the failing call to `fwrite()` or `fprintf()`.
>>   */
>> -static int write_packed_entry(FILE *fh, const char *refname,
>> -			      const struct object_id *oid,
>> +static int write_packed_entry(FILE *fh, const char *raw, size_t raw_len,
>> +			      const char *refname, const struct object_id *oid,
>>  			      const struct object_id *peeled)
>
> I'm not convinced it's worth to have both ways of writing in a single
> function.
>
> I rather keep this function as-is and add a function:
>
>     static int write_packed_entry_preformatted(FILE *fh,
>                                                const char *line,
>                                                size_t line_len)
>     {
>     	if (fwrite(raw, raw_len, 1, fh) != 1)
>     			return -1;
>     	return 0;
>     }
>
> Or maybe even:
>
>     static int write_packed_entry_from_iter(FILE *fh,
>                                             struct packed_ref_iterator *packed_iter)
>     {
>     	size_t ret = fwrite(packed_iter->record_start,
>                             packed_iter->pos - packed_iter->record_start,
>                             1, fh);
>         if (ret != 1)
>     			return -1;
>     	return 0;
>     }
>

I did it cause it was small enough, but I don't have strong opinions. So let's add another function, perhaps:

static int write_packed_entry_raw(FILE *fh, const char *entry, size_t len)
{
	if (fwrite(entry, len, 1, fh) != 1)
		return -1;
	return 0;
}
[snip]
Show 6 quoted lines
>> +			return -1;
>> +	} else if (fprintf(fh, "%s %s\n", oid_to_hex(oid), refname) < 0 ||
>> +		   (peeled && fprintf(fh, "^%s\n", oid_to_hex(peeled)) < 0)) {
>
> For what's it's worth, I think it's really ugly to do this in a single
> if() statement, but that just could be me.

I agree, but I don't want to change existing code in this patch as that would simply be a distraction from the purpose.

[snip]
>
> --
> Laters,
> Toon
Thanks for the review.
Previous: Toon ClaesNext: Karthik Nayak
Message 3 of 15 in “packed-refs: use `fwrite()` when passing refs verbatim”
  1. packed-refs: use `fwrite()` when passing refs verbatimKarthik Nayak, Sep 30, 2026
  2. Toon ClaesOct 1, 2026
  3. Karthik NayakOct 2, 2026
  4. packed-refs: use `fwrite()` when passing refs verbatimKarthik Nayak, Oct 2, 2026
  5. Toon ClaesOct 5, 2026
  6. packed-refs: use `fwrite()` when passing refs verbatimKarthik Nayak, Oct 6, 2026
  7. Karthik NayakOct 6, 2026
  8. Patrick SteinhardtOct 6, 2026
  9. Junio C HamanoOct 6, 2026
  10. Karthik NayakOct 6, 2026
  11. Karthik NayakOct 6, 2026
  12. Patrick SteinhardtOct 7, 2026
  13. Karthik NayakOct 7, 2026
  14. packed-refs: use `fwrite()` when passing refs verbatimKarthik Nayak, Oct 7, 2026
  15. Patrick SteinhardtOct 7, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.