odb: support setting mtime when writing objects

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jul 17, 2026 at 11:32 UTC 8d9135dce3be50c961f23f5e89352b0fd1d61117
11 files changed +67 -35
builtin/pack-objects.c
+1 -1
@@ -4642,7 +4642,7 @@ static void loosen_unused_packed_objects(void)
4642 !has_sha1_pack_kept_or_nonlocal(&oid) &&
4643 !loosened_object_can_be_discarded(&oid, p->mtime)) {
4644 if (force_object_loose(the_repository->objects->sources,
4645 - &oid, p->mtime))
4645 + &oid, &p->mtime))
4646 die(_("unable to force loose object"));
4647 loosened_objects_nr++;
4648 }
object-file.c
+20 -9
@@ -67,9 +67,17 @@ const char *odb_loose_path(struct odb_source_loose *loose,
67 }
68
69 /* Returns 1 if we have successfully freshened the file, 0 otherwise. */
70 -static int freshen_file(const char *fn)
70 +static int freshen_file(const char *fn, const time_t *mtime)
71 {
72 - return !utime(fn, NULL);
72 + struct utimbuf times, *timesp = NULL;
73 +
74 + if (mtime) {
75 + times.actime = *mtime;
76 + times.modtime = *mtime;
77 + timesp = &times;
78 + }
79 +
80 + return !utime(fn, timesp);
81 }
82
83 /*
@@ -79,11 +87,12 @@ static int freshen_file(const char *fn)
87 * either does not exist on disk, or has a stale mtime and may be subject to
88 * pruning).
89 */
82 -int check_and_freshen_file(const char *fn, int freshen)
90 +int check_and_freshen_file(const char *fn, int freshen,
91 + const time_t *mtime)
92 {
93 if (access(fn, F_OK))
94 return 0;
86 - if (freshen && !freshen_file(fn))
95 + if (freshen && !freshen_file(fn, mtime))
96 return 0;
97 return 1;
98 }
@@ -706,7 +715,7 @@ static int end_loose_object_common(struct odb_source_loose *loose,
715 int write_loose_object(struct odb_source_loose *loose,
716 const struct object_id *oid, char *hdr,
717 int hdrlen, const void *buf, unsigned long len,
709 - time_t mtime, unsigned flags)
718 + const time_t *mtime, unsigned flags)
719 {
720 int fd, ret;
721 unsigned char compressed[4096];
@@ -751,9 +760,11 @@ int write_loose_object(struct odb_source_loose *loose,
760 close_loose_object(loose, fd, tmp_file.buf);
761
762 if (mtime) {
754 - struct utimbuf utb;
755 - utb.actime = mtime;
756 - utb.modtime = mtime;
763 + struct utimbuf utb = {
764 + .actime = *mtime,
765 + .modtime = *mtime,
766 + };
767 +
768 if (utime(tmp_file.buf, &utb) < 0 &&
769 !(flags & ODB_WRITE_OBJECT_SILENT))
770 warning_errno(_("failed utime() on %s"), tmp_file.buf);
@@ -883,7 +894,7 @@ cleanup:
894 }
895
896 int force_object_loose(struct odb_source *source,
886 - const struct object_id *oid, time_t mtime)
897 + const struct object_id *oid, const time_t *mtime)
898 {
899 struct odb_source_files *files = odb_source_files_downcast(source);
900 const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo;
object-file.h
+5 -3
@@ -99,7 +99,8 @@ int format_object_header(char *str, size_t size, enum object_type type,
99 size_t objsize);
100
101 int force_object_loose(struct odb_source *source,
102 - const struct object_id *oid, time_t mtime);
102 + const struct object_id *oid,
103 + const time_t *mtime);
104
105 /**
106 * 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,
138 int write_loose_object(struct odb_source_loose *loose,
139 const struct object_id *oid, char *hdr,
140 int hdrlen, const void *buf, unsigned long len,
140 - time_t mtime, unsigned flags);
141 + const time_t *mtime, unsigned flags);
142
143 /* Helper to check and "touch" a file */
143 -int check_and_freshen_file(const char *fn, int freshen);
144 +int check_and_freshen_file(const char *fn, int freshen,
145 + const time_t *mtime);
146
147 /*
148 * Open the loose object at path, check its hash, and return the contents,
odb.c
+3 -3
@@ -738,7 +738,7 @@ int odb_pretend_object(struct object_database *odb,
738 return 0;
739
740 return odb_source_write_object(odb->inmemory_objects,
741 - buf, len, type, oid, NULL, 0);
741 + buf, len, type, oid, NULL, NULL, 0);
742 }
743
744 void *odb_read_object(struct object_database *odb,
@@ -829,7 +829,7 @@ int odb_freshen_object(struct object_database *odb,
829 struct odb_source *source;
830 odb_prepare_alternates(odb);
831 for (source = odb->sources; source; source = source->next)
832 - if (odb_source_freshen_object(source, oid))
832 + if (odb_source_freshen_object(source, oid, NULL))
833 return 1;
834 return 0;
835 }
@@ -1024,7 +1024,7 @@ int odb_write_object_ext(struct object_database *odb,
1024 }
1025
1026 return odb_source_write_object(odb->sources, buf, len, type,
1027 - oid, compat_oid_p, flags);
1027 + oid, compat_oid_p, NULL, flags);
1028 }
1029
1030 int odb_write_object_stream(struct object_database *odb,
odb/source-files.c
+6 -4
@@ -150,11 +150,12 @@ out:
150 }
151
152 static int odb_source_files_freshen_object(struct odb_source *source,
153 - const struct object_id *oid)
153 + const struct object_id *oid,
154 + const time_t *mtime)
155 {
156 struct odb_source_files *files = odb_source_files_downcast(source);
156 - if (odb_source_freshen_object(&files->packed->base, oid) ||
157 - odb_source_freshen_object(&files->loose->base, oid))
157 + if (odb_source_freshen_object(&files->packed->base, oid, mtime) ||
158 + odb_source_freshen_object(&files->loose->base, oid, mtime))
159 return 1;
160 return 0;
161 }
@@ -164,11 +165,12 @@ static int odb_source_files_write_object(struct odb_source *source,
165 enum object_type type,
166 const struct object_id *oid,
167 const struct object_id *compat_oid,
168 + const time_t *mtime,
169 enum odb_write_object_flags flags)
170 {
171 struct odb_source_files *files = odb_source_files_downcast(source);
172 return odb_source_write_object(&files->loose->base, buf, len, type,
171 - oid, compat_oid, flags);
173 + oid, compat_oid, mtime, flags);
174 }
175
176 static int odb_source_files_write_object_stream(struct odb_source *source,
odb/source-inmemory.c
+4 -2
@@ -232,6 +232,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source,
232 enum object_type type,
233 const struct object_id *oid,
234 const struct object_id *compat_oid UNUSED,
235 + const time_t *mtime UNUSED,
236 enum odb_write_object_flags flags UNUSED)
237 {
238 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,
287 hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid);
288
289 ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid,
289 - NULL, 0);
290 + NULL, NULL, 0);
291 if (ret < 0)
292 goto out;
293
@@ -296,7 +297,8 @@ out:
297 }
298
299 static int odb_source_inmemory_freshen_object(struct odb_source *source,
299 - const struct object_id *oid)
300 + const struct object_id *oid,
301 + const time_t *mtime UNUSED)
302 {
303 struct odb_source_inmemory *inmemory = odb_source_inmemory_downcast(source);
304 if (find_cached_object(inmemory, oid))
odb/source-loose.c
+5 -3
@@ -574,12 +574,13 @@ out:
574 }
575
576 static int odb_source_loose_freshen_object(struct odb_source *source,
577 - const struct object_id *oid)
577 + const struct object_id *oid,
578 + const time_t *mtime)
579 {
580 struct odb_source_loose *loose = odb_source_loose_downcast(source);
581 static struct strbuf path = STRBUF_INIT;
582 odb_loose_path(loose, &path, oid);
582 - return !!check_and_freshen_file(path.buf, 1);
583 + return !!check_and_freshen_file(path.buf, 1, mtime);
584 }
585
586 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,
588 enum object_type type,
589 const struct object_id *oid,
590 const struct object_id *compat_oid,
591 + const time_t *mtime,
592 enum odb_write_object_flags flags)
593 {
594 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,
597
598 hdrlen = format_object_header(hdr, sizeof(hdr), type, len);
599
598 - if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags))
600 + if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, mtime, flags))
601 return -1;
602
603 if (compat_oid)
odb/source-packed.c
+11 -2
@@ -507,18 +507,26 @@ static int odb_source_packed_find_abbrev_len(struct odb_source *source,
507 }
508
509 static int odb_source_packed_freshen_object(struct odb_source *source,
510 - const struct object_id *oid)
510 + const struct object_id *oid,
511 + const time_t *mtime)
512 {
513 struct odb_source_packed *packed = odb_source_packed_downcast(source);
514 + struct utimbuf times, *timesp = NULL;
515 struct pack_entry e;
516
517 + if (mtime) {
518 + times.actime = *mtime;
519 + times.modtime = *mtime;
520 + timesp = &times;
521 + }
522 +
523 if (!find_pack_entry(packed, oid, &e))
524 return 0;
525 if (e.p->is_cruft)
526 return 0;
527 if (e.p->freshened)
528 return 1;
521 - if (utime(e.p->pack_name, NULL))
529 + if (utime(e.p->pack_name, timesp))
530 return 0;
531 e.p->freshened = 1;
532
@@ -531,6 +539,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED,
539 enum object_type type UNUSED,
540 const struct object_id *oid UNUSED,
541 const struct object_id *compat_oid UNUSED,
542 + const time_t *mtime UNUSED,
543 unsigned flags UNUSED)
544 {
545 return error("packed backend cannot write objects");
odb/source.h
+8 -4
@@ -190,7 +190,8 @@ struct odb_source {
190 * has been freshened.
191 */
192 int (*freshen_object)(struct odb_source *source,
193 - const struct object_id *oid);
193 + const struct object_id *oid,
194 + const time_t *mtime);
195
196 /*
197 * This callback is expected to persist the given object into the
@@ -208,6 +209,7 @@ struct odb_source {
209 enum object_type type,
210 const struct object_id *oid,
211 const struct object_id *compat_oid,
212 + const time_t *mtime,
213 enum odb_write_object_flags flags);
214
215 /*
@@ -403,9 +405,10 @@ static inline int odb_source_find_abbrev_len(struct odb_source *source,
405 * not exist.
406 */
407 static inline int odb_source_freshen_object(struct odb_source *source,
406 - const struct object_id *oid)
408 + const struct object_id *oid,
409 + const time_t *mtime)
410 {
408 - return source->freshen_object(source, oid);
411 + return source->freshen_object(source, oid, mtime);
412 }
413
414 /*
@@ -418,10 +421,11 @@ static inline int odb_source_write_object(struct odb_source *source,
421 enum object_type type,
422 const struct object_id *oid,
423 const struct object_id *compat_oid,
424 + const time_t *mtime,
425 enum odb_write_object_flags flags)
426 {
427 return source->write_object(source, buf, len, type, oid,
424 - compat_oid, flags);
428 + compat_oid, mtime, flags);
429 }
430
431 /*
read-cache.c
+1 -1
@@ -2342,7 +2342,7 @@ unmap:
2342 */
2343 static void freshen_shared_index(const char *shared_index, int warn)
2344 {
2345 - if (!check_and_freshen_file(shared_index, 1) && warn)
2345 + if (!check_and_freshen_file(shared_index, 1, NULL) && warn)
2346 warning(_("could not freshen shared index '%s'"), shared_index);
2347 }
2348
t/unit-tests/u-odb-inmemory.c
+3 -3
@@ -45,7 +45,7 @@ static void cl_assert_write_object(struct odb_source_inmemory *source,
45 size_t content_len = strlen(content);
46 hash_object_file(repo.hash_algo, content, content_len, type, oid);
47 cl_must_pass(odb_source_write_object(&source->base, content, content_len,
48 - type, oid, NULL, 0));
48 + type, oid, NULL, NULL, 0));
49 }
50
51 void test_odb_inmemory__initialize(void)
@@ -256,11 +256,11 @@ void test_odb_inmemory__freshen_object(void)
256 const char *end;
257
258 cl_must_pass(parse_oid_hex_algop(RANDOM_OID, &oid, &end, repo.hash_algo));
259 - cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid), 0);
259 + cl_assert_equal_i(odb_source_freshen_object(&source->base, &oid, NULL), 0);
260
261 cl_assert_write_object(source, "foobar", OBJ_BLOB, &written_oid);
262 cl_assert_equal_i(odb_source_freshen_object(&source->base,
263 - &written_oid), 1);
263 + &written_oid, NULL), 1);
264
265 odb_source_free(&source->base);
266 }