Skip to content

Commit 0fd1ffa

Browse files
committed
copy: prefer reflinks for file copies
Git currently copies files byte-for-byte through copy_file(). Local clones separately try to hardlink object files before falling back to copying them. On filesystems that support copy-on-write cloning, a reflink can share the underlying storage without making the source and destination names refer to the same inode. This provides most of the space and I/O benefits of hardlinks while allowing either file to be replaced or modified independently. On Linux, try FICLONE before performing a byte-for-byte copy. Treat reflinking as an optimization: if the ioctl is unavailable or fails, remove the partial destination and use the existing copy path. For local clones, keep hardlinks as the first choice because object files are immutable. If a hardlink cannot be created, try a reflink before falling back to a byte-for-byte copy. The resulting order is: - hardlink, unless --no-hardlinks was requested; - reflink; - byte-for-byte copy. Preserve source timestamps when reflinking local object files. This matches the previous hardlink and copy behavior and is important for the expiry decisions made by prune and gc. Add an LD_PRELOAD-based test helper that can force hardlink failure and make FICLONE succeed, report EOPNOTSUPP, or report another error. This exercises the reflink and fallback paths even when the test filesystem does not support reflinks. Cover generic file copying, local clones, --no-hardlinks, hardlink preference, reflink fallback, byte-copy fallback, object integrity, and timestamp preservation. The focused tests pass on three independent filesystems: - ZFS at /home; - ext4 at /tmp; - tmpfs at /dev/shm. The complete Git test suite also passes on all three filesystems with no unexpected failures. Signed-off-by: Vlad Petric <vlad@drpetric.com>
1 parent 010afd3 commit 0fd1ffa

12 files changed

Lines changed: 306 additions & 14 deletions

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ TEST_BUILTINS_OBJS += test-bundle-uri.o
814814
TEST_BUILTINS_OBJS += test-cache-tree.o
815815
TEST_BUILTINS_OBJS += test-chmtime.o
816816
TEST_BUILTINS_OBJS += test-config.o
817+
TEST_BUILTINS_OBJS += test-copy-file.o
817818
TEST_BUILTINS_OBJS += test-crontab.o
818819
TEST_BUILTINS_OBJS += test-csprng.o
819820
TEST_BUILTINS_OBJS += test-date.o

builtin/clone.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
310310
if (unlink(dest->buf) && errno != ENOENT)
311311
die_errno(_("failed to unlink '%s'"), dest->buf);
312312
if (!option_no_hardlinks) {
313+
int link_errno;
314+
313315
if (!link(src->buf, dest->buf)) {
314316
struct stat st;
315317

@@ -331,6 +333,16 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
331333

332334
continue;
333335
}
336+
link_errno = errno;
337+
if (!copy_file_reflink_with_time(the_repository, dest->buf,
338+
src->buf, 0666))
339+
continue;
340+
errno = link_errno;
341+
} else if (!copy_file_reflink_with_time(the_repository, dest->buf,
342+
src->buf, 0666)) {
343+
continue;
344+
}
345+
if (!option_no_hardlinks) {
334346
if (option_local > 0)
335347
die_errno(_("failed to create link '%s'"), dest->buf);
336348
option_no_hardlinks = 1;

copy.c

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
#include "strbuf.h"
66
#include "abspath.h"
77

8+
#ifdef __linux__
9+
#include <sys/ioctl.h>
10+
11+
#define FICLONE _IOW(0x94, 9, int)
12+
#endif
13+
814
int copy_fd(int ifd, int ofd)
915
{
1016
while (1) {
@@ -33,19 +39,9 @@ static int copy_times(const char *dst, const char *src)
3339
return 0;
3440
}
3541

36-
int copy_file(struct repository *repo,
37-
const char *dst, const char *src, int mode)
42+
static int finish_copy(struct repository *repo, const char *dst,
43+
int fdi, int fdo, int status)
3844
{
39-
int fdi, fdo, status;
40-
41-
mode = (mode & 0111) ? 0777 : 0666;
42-
if ((fdi = open(src, O_RDONLY)) < 0)
43-
return fdi;
44-
if ((fdo = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode)) < 0) {
45-
close(fdi);
46-
return fdo;
47-
}
48-
status = copy_fd(fdi, fdo);
4945
switch (status) {
5046
case COPY_READ_ERROR:
5147
error_errno("copy-fd: read returned");
@@ -64,6 +60,82 @@ int copy_file(struct repository *repo,
6460
return status;
6561
}
6662

63+
int copy_file_reflink(struct repository *repo,
64+
const char *dst, const char *src, int mode)
65+
{
66+
#ifndef FICLONE
67+
(void)repo;
68+
(void)dst;
69+
(void)src;
70+
(void)mode;
71+
errno = ENOTSUP;
72+
return -1;
73+
#else
74+
int fdi, fdo, status;
75+
76+
mode = (mode & 0111) ? 0777 : 0666;
77+
if ((fdi = open(src, O_RDONLY)) < 0)
78+
return fdi;
79+
if ((fdo = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode)) < 0) {
80+
close(fdi);
81+
return fdo;
82+
}
83+
status = ioctl(fdo, FICLONE, fdi);
84+
if (status) {
85+
int saved_errno = errno;
86+
87+
close(fdi);
88+
close(fdo);
89+
unlink(dst);
90+
errno = saved_errno;
91+
return -1;
92+
}
93+
94+
return finish_copy(repo, dst, fdi, fdo, 0);
95+
#endif
96+
}
97+
98+
int copy_file_reflink_with_time(struct repository *repo,
99+
const char *dst, const char *src, int mode)
100+
{
101+
int saved_errno;
102+
103+
if (copy_file_reflink(repo, dst, src, mode))
104+
return -1;
105+
if (!copy_times(dst, src))
106+
return 0;
107+
108+
saved_errno = errno;
109+
unlink(dst);
110+
errno = saved_errno;
111+
return -1;
112+
}
113+
114+
static int copy_file_contents(struct repository *repo,
115+
const char *dst, const char *src, int mode)
116+
{
117+
int fdi, fdo;
118+
119+
mode = (mode & 0111) ? 0777 : 0666;
120+
if ((fdi = open(src, O_RDONLY)) < 0)
121+
return fdi;
122+
if ((fdo = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode)) < 0) {
123+
close(fdi);
124+
return fdo;
125+
}
126+
127+
return finish_copy(repo, dst, fdi, fdo, copy_fd(fdi, fdo));
128+
}
129+
130+
int copy_file(struct repository *repo,
131+
const char *dst, const char *src, int mode)
132+
{
133+
if (!copy_file_reflink(repo, dst, src, mode))
134+
return 0;
135+
136+
return copy_file_contents(repo, dst, src, mode);
137+
}
138+
67139
int copy_file_with_time(struct repository *repo,
68140
const char *dst, const char *src, int mode)
69141
{

copy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ struct repository;
66
#define COPY_READ_ERROR (-2)
77
#define COPY_WRITE_ERROR (-3)
88
int copy_fd(int ifd, int ofd);
9+
int copy_file_reflink(struct repository *repo,
10+
const char *dst, const char *src, int mode);
11+
int copy_file_reflink_with_time(struct repository *repo,
12+
const char *dst, const char *src, int mode);
913
int copy_file(struct repository *repo,
1014
const char *dst, const char *src, int mode);
1115
int copy_file_with_time(struct repository *repo,

t/helper/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ test_tool_sources = [
77
'test-cache-tree.c',
88
'test-chmtime.c',
99
'test-config.c',
10+
'test-copy-file.c',
1011
'test-crontab.c',
1112
'test-csprng.c',
1213
'test-date.c',

t/helper/test-copy-file.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#define USE_THE_REPOSITORY_VARIABLE
2+
3+
#include "test-tool.h"
4+
#include "copy.h"
5+
#include "environment.h"
6+
#include "repository.h"
7+
8+
int cmd__copy_file(int argc, const char **argv)
9+
{
10+
if (argc != 3)
11+
return 129;
12+
return copy_file(the_repository, argv[2], argv[1], 0666) ? 1 : 0;
13+
}

t/helper/test-fake-reflink.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#define _GNU_SOURCE
2+
#include <dlfcn.h>
3+
#include <errno.h>
4+
#include <fcntl.h>
5+
#include <stdarg.h>
6+
#include <stdlib.h>
7+
#include <string.h>
8+
#include <sys/ioctl.h>
9+
#include <unistd.h>
10+
11+
#define FICLONE _IOW(0x94, 9, int)
12+
13+
static int emulate_clone(int dst, int src)
14+
{
15+
char buf[8192];
16+
off_t pos = 0;
17+
18+
for (;;) {
19+
ssize_t nr = pread(src, buf, sizeof(buf), pos);
20+
if (nr < 0)
21+
return -1;
22+
if (!nr)
23+
return ftruncate(dst, pos);
24+
if (pwrite(dst, buf, nr, pos) != nr)
25+
return -1;
26+
pos += nr;
27+
}
28+
}
29+
30+
static void log_clone_attempt(void)
31+
{
32+
const char *path = getenv("GIT_TEST_FICLONE_LOG");
33+
int fd;
34+
35+
if (!path)
36+
return;
37+
fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666);
38+
if (fd < 0)
39+
return;
40+
write(fd, "FICLONE\n", 8);
41+
close(fd);
42+
}
43+
44+
int ioctl(int fd, unsigned long request, ...)
45+
{
46+
static int (*real_ioctl)(int, unsigned long, ...);
47+
va_list ap;
48+
unsigned long arg;
49+
const char *mode;
50+
51+
va_start(ap, request);
52+
arg = va_arg(ap, unsigned long);
53+
va_end(ap);
54+
55+
if (request != FICLONE) {
56+
if (!real_ioctl)
57+
real_ioctl = dlsym(RTLD_NEXT, "ioctl");
58+
return real_ioctl(fd, request, arg);
59+
}
60+
61+
log_clone_attempt();
62+
mode = getenv("GIT_TEST_FICLONE");
63+
if (!mode || !strcmp(mode, "real")) {
64+
if (!real_ioctl)
65+
real_ioctl = dlsym(RTLD_NEXT, "ioctl");
66+
return real_ioctl(fd, request, arg);
67+
}
68+
if (!strcmp(mode, "success"))
69+
return emulate_clone(fd, (int)arg);
70+
errno = !strcmp(mode, "unsupported") ? EOPNOTSUPP : EIO;
71+
return -1;
72+
}
73+
74+
int link(const char *oldpath, const char *newpath)
75+
{
76+
static int (*real_link)(const char *, const char *);
77+
78+
if (!getenv("GIT_TEST_LINK_FAILURE")) {
79+
if (!real_link)
80+
real_link = dlsym(RTLD_NEXT, "link");
81+
return real_link(oldpath, newpath);
82+
}
83+
errno = EPERM;
84+
return -1;
85+
}

t/helper/test-tool.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ static struct test_cmd cmds[] = {
1717
{ "cache-tree", cmd__cache_tree },
1818
{ "chmtime", cmd__chmtime },
1919
{ "config", cmd__config },
20+
{ "copy-file", cmd__copy_file },
2021
{ "crontab", cmd__crontab },
2122
{ "csprng", cmd__csprng },
2223
{ "date", cmd__date },

t/helper/test-tool.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ int cmd__bundle_uri(int argc, const char **argv);
1010
int cmd__cache_tree(int argc, const char **argv);
1111
int cmd__chmtime(int argc, const char **argv);
1212
int cmd__config(int argc, const char **argv);
13+
int cmd__copy_file(int argc, const char **argv);
1314
int cmd__crontab(int argc, const char **argv);
1415
int cmd__csprng(int argc, const char **argv);
1516
int cmd__date(int argc, const char **argv);

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ integration_tests = [
126126
't0091-bugreport.sh',
127127
't0092-diagnose.sh',
128128
't0093-verify-cache-df-gap.sh',
129+
't0094-reflink.sh',
129130
't0095-bloom.sh',
130131
't0100-previous.sh',
131132
't0101-at-syntax.sh',

0 commit comments

Comments
 (0)