Skip to content

Commit 714bb21

Browse files
mjcheethamdscho
authored andcommitted
maintenance: add cache-local-objects maintenance task
Introduce a new maintenance task, `cache-local-objects`, that operates on Scalar or VFS for Git repositories with a per-volume, shared object cache (specified by `gvfs.sharedCache`) to migrate packfiles and loose objects from the repository object directory to the shared cache. Older versions of `microsoft/git` incorrectly placed packfiles in the repository object directory instead of the shared cache; this task will help clean up existing clones impacted by that issue. Migration of packfiles involves the following steps for each pack: 1. Hardlink (or copy): a. the .pack file b. the .keep file c. the .rev file 2. Move (or copy + delete) the .idx file 3. Delete/unlink: a. the .pack file b. the .keep file c. the .rev file Moving the index file after the others ensures the pack is not read from the new cache directory until all associated files (rev, keep) exist in the cache directory also. Moving loose objects operates as a move, or copy + delete. Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
1 parent db35c6f commit 714bb21

3 files changed

Lines changed: 334 additions & 0 deletions

File tree

Documentation/git-maintenance.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ task:
7070
* `prefetch`: hourly.
7171
* `loose-objects`: daily.
7272
* `incremental-repack`: daily.
73+
* `cache-local-objects`: weekly.
7374
--
7475
+
7576
`git maintenance register` will also disable foreground maintenance by
@@ -185,6 +186,13 @@ worktree-prune::
185186
The `worktree-prune` task deletes stale or broken worktrees. See
186187
linkgit:git-worktree[1] for more information.
187188

189+
cache-local-objects::
190+
The `cache-local-objects` task only operates on Scalar or VFS for Git
191+
repositories (cloned with either `scalar clone` or `gvfs clone`) that
192+
have the `gvfs.sharedCache` configuration setting present. This task
193+
migrates pack files and loose objects from the repository's object
194+
directory in to the shared volume cache.
195+
188196
OPTIONS
189197
-------
190198
--auto::

builtin/gc.c

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
#define USE_THE_REPOSITORY_VARIABLE
1414
#define DISABLE_SIGN_COMPARE_WARNINGS
1515

16+
#include "git-compat-util.h"
1617
#include "builtin.h"
1718
#include "abspath.h"
19+
#include "copy.h"
1820
#include "date.h"
1921
#include "dir.h"
2022
#include "environment.h"
@@ -264,6 +266,7 @@ enum maintenance_task_label {
264266
TASK_REFLOG_EXPIRE,
265267
TASK_WORKTREE_PRUNE,
266268
TASK_RERERE_GC,
269+
TASK_CACHE_LOCAL_OBJS,
267270

268271
/* Leave as final value */
269272
TASK__COUNT
@@ -1697,6 +1700,188 @@ static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED)
16971700
return ret;
16981701
}
16991702

1703+
static void link_or_copy_or_die(const char *src, const char *dst)
1704+
{
1705+
if (!link(src, dst))
1706+
return;
1707+
1708+
/* Use copy operation if src and dst are on different file systems. */
1709+
if (errno != EXDEV)
1710+
warning_errno(_("failed to link '%s' to '%s'"), src, dst);
1711+
1712+
if (copy_file(dst, src, 0444))
1713+
die_errno(_("failed to copy '%s' to '%s'"), src, dst);
1714+
}
1715+
1716+
static void rename_or_copy_or_die(const char *src, const char *dst)
1717+
{
1718+
if (!rename(src, dst))
1719+
return;
1720+
1721+
/* Use copy and delete if src and dst are on different file systems. */
1722+
if (errno != EXDEV)
1723+
warning_errno(_("failed to move '%s' to '%s'"), src, dst);
1724+
1725+
if (copy_file(dst, src, 0444))
1726+
die_errno(_("failed to copy '%s' to '%s'"), src, dst);
1727+
1728+
if (unlink(src))
1729+
die_errno(_("failed to delete '%s'"), src);
1730+
}
1731+
1732+
static void migrate_pack(const char *srcdir, const char *dstdir,
1733+
const char *pack_filename)
1734+
{
1735+
size_t basenamelen, srclen, dstlen;
1736+
struct strbuf src = STRBUF_INIT, dst = STRBUF_INIT;
1737+
struct {
1738+
const char *ext;
1739+
unsigned move:1;
1740+
} files[] = {
1741+
{".pack", 0},
1742+
{".keep", 0},
1743+
{".rev", 0},
1744+
{".idx", 1}, /* The index file must be atomically moved last. */
1745+
};
1746+
1747+
trace2_region_enter("maintenance", "migrate_pack", the_repository);
1748+
1749+
basenamelen = strlen(pack_filename) - 5; /* .pack */
1750+
strbuf_addstr(&src, srcdir);
1751+
strbuf_addch(&src, '/');
1752+
strbuf_add(&src, pack_filename, basenamelen);
1753+
strbuf_addstr(&src, ".idx");
1754+
1755+
/* A pack without an index file is not yet ready to be migrated. */
1756+
if (!file_exists(src.buf))
1757+
goto cleanup;
1758+
1759+
strbuf_setlen(&src, src.len - 4 /* .idx */);
1760+
strbuf_addstr(&dst, dstdir);
1761+
strbuf_addch(&dst, '/');
1762+
strbuf_add(&dst, pack_filename, basenamelen);
1763+
1764+
srclen = src.len;
1765+
dstlen = dst.len;
1766+
1767+
/* Move or copy files from the source directory to the destination. */
1768+
for (size_t i = 0; i < ARRAY_SIZE(files); i++) {
1769+
strbuf_setlen(&src, srclen);
1770+
strbuf_addstr(&src, files[i].ext);
1771+
1772+
if (!file_exists(src.buf))
1773+
continue;
1774+
1775+
strbuf_setlen(&dst, dstlen);
1776+
strbuf_addstr(&dst, files[i].ext);
1777+
1778+
if (files[i].move)
1779+
rename_or_copy_or_die(src.buf, dst.buf);
1780+
else
1781+
link_or_copy_or_die(src.buf, dst.buf);
1782+
}
1783+
1784+
/*
1785+
* Now the pack and all associated files exist at the destination we can
1786+
* now clean up the files in the source directory.
1787+
*/
1788+
for (size_t i = 0; i < ARRAY_SIZE(files); i++) {
1789+
/* Files that were moved rather than copied have no clean up. */
1790+
if (files[i].move)
1791+
continue;
1792+
1793+
strbuf_setlen(&src, srclen);
1794+
strbuf_addstr(&src, files[i].ext);
1795+
1796+
/* Files that never existed in originally have no clean up.*/
1797+
if (!file_exists(src.buf))
1798+
continue;
1799+
1800+
if (unlink(src.buf))
1801+
warning_errno(_("failed to delete '%s'"), src.buf);
1802+
}
1803+
1804+
cleanup:
1805+
strbuf_release(&src);
1806+
strbuf_release(&dst);
1807+
1808+
trace2_region_leave("maintenance", "migrate_pack", the_repository);
1809+
}
1810+
1811+
static void move_pack_to_shared_cache(const char *full_path, size_t full_path_len,
1812+
const char *file_name, void *data)
1813+
{
1814+
char *srcdir;
1815+
const char *dstdir = (const char *)data;
1816+
1817+
/* We only care about the actual pack files here.
1818+
* The associated .idx, .keep, .rev files will be copied in tandem
1819+
* with the pack file, with the index file being moved last.
1820+
* The original locations of the non-index files will only deleted
1821+
* once all other files have been copied/moved.
1822+
*/
1823+
if (!ends_with(file_name, ".pack"))
1824+
return;
1825+
1826+
srcdir = xstrndup(full_path, full_path_len - strlen(file_name) - 1);
1827+
1828+
migrate_pack(srcdir, dstdir, file_name);
1829+
1830+
free(srcdir);
1831+
}
1832+
1833+
static int move_loose_object_to_shared_cache(const struct object_id *oid,
1834+
const char *path,
1835+
UNUSED void *data)
1836+
{
1837+
struct stat st;
1838+
struct strbuf dst = STRBUF_INIT;
1839+
char *hex = oid_to_hex(oid);
1840+
1841+
strbuf_addf(&dst, "%s/%.2s/", shared_object_dir, hex);
1842+
1843+
if (stat(dst.buf, &st)) {
1844+
if (mkdir(dst.buf, 0777))
1845+
die_errno(_("failed to create directory '%s'"), dst.buf);
1846+
} else if (!S_ISDIR(st.st_mode))
1847+
die(_("expected '%s' to be a directory"), dst.buf);
1848+
1849+
strbuf_addstr(&dst, hex+2);
1850+
rename_or_copy_or_die(path, dst.buf);
1851+
1852+
strbuf_release(&dst);
1853+
return 0;
1854+
}
1855+
1856+
static int maintenance_task_cache_local_objs(UNUSED struct maintenance_run_opts *opts,
1857+
UNUSED struct gc_config *cfg)
1858+
{
1859+
struct strbuf dstdir = STRBUF_INIT;
1860+
struct repository *r = the_repository;
1861+
int ret = 0;
1862+
1863+
/* This task is only applicable with a VFS/Scalar shared cache. */
1864+
if (!shared_object_dir)
1865+
return 0;
1866+
1867+
/* If the dest is the same as the local odb path then we do nothing. */
1868+
if (!fspathcmp(r->objects->sources->path, shared_object_dir))
1869+
goto cleanup;
1870+
1871+
strbuf_addf(&dstdir, "%s/pack", shared_object_dir);
1872+
1873+
for_each_file_in_pack_dir(r->objects->sources->path, move_pack_to_shared_cache,
1874+
dstdir.buf);
1875+
1876+
ret = for_each_loose_file_in_source(r->objects->sources,
1877+
move_loose_object_to_shared_cache,
1878+
NULL, NULL, NULL);
1879+
1880+
cleanup:
1881+
strbuf_release(&dstdir);
1882+
return ret;
1883+
}
1884+
17001885
typedef int (*maintenance_task_fn)(struct maintenance_run_opts *opts,
17011886
struct gc_config *cfg);
17021887
typedef int (*maintenance_auto_fn)(struct gc_config *cfg);
@@ -1775,6 +1960,10 @@ static const struct maintenance_task tasks[] = {
17751960
.background = maintenance_task_rerere_gc,
17761961
.auto_condition = rerere_gc_condition,
17771962
},
1963+
[TASK_CACHE_LOCAL_OBJS] = {
1964+
"cache-local-objects",
1965+
maintenance_task_cache_local_objs,
1966+
},
17781967
};
17791968

17801969
enum task_phase {
@@ -1901,6 +2090,10 @@ static const struct maintenance_strategy incremental_strategy = {
19012090
.type = MAINTENANCE_TYPE_SCHEDULED,
19022091
.schedule = SCHEDULE_WEEKLY,
19032092
},
2093+
[TASK_CACHE_LOCAL_OBJS] = {
2094+
.type = MAINTENANCE_TYPE_SCHEDULED,
2095+
.schedule = SCHEDULE_WEEKLY,
2096+
},
19042097
/*
19052098
* Historically, the "incremental" strategy was only available
19062099
* in the context of scheduled maintenance when set up via

t/t7900-maintenance.sh

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ test_systemd_analyze_verify () {
3434
fi
3535
}
3636

37+
test_import_packfile () {
38+
printf "blob\ndata <<END\n%s\nEND\n\n" 1 2 3 4 5 | \
39+
git -c fastimport.unpackLimit=0 fast-import
40+
}
41+
42+
test_get_packdir_files() {
43+
if [ "$#" -eq 0 ]; then
44+
find .git/objects/pack -type f
45+
else
46+
for arg in "$@"; do
47+
find .git/objects/pack -type f -name $arg
48+
done
49+
fi
50+
}
51+
52+
test_get_loose_object_files () {
53+
find .git/objects -type f -path '.git/objects/??/*'
54+
}
55+
3756
test_expect_success 'help text' '
3857
test_expect_code 129 git maintenance -h >actual &&
3958
test_grep "usage: git maintenance <subcommand>" actual &&
@@ -1521,4 +1540,118 @@ test_expect_success 'maintenance aborts with existing lock file' '
15211540
test_grep "Another scheduled git-maintenance(1) process seems to be running" err
15221541
'
15231542

1543+
test_expect_success 'cache-local-objects task with no shared cache no op' '
1544+
test_when_finished "rm -rf repo" &&
1545+
git init repo &&
1546+
(
1547+
cd repo &&
1548+
1549+
test_commit something &&
1550+
git config set maintenance.gc.enabled false &&
1551+
git config set maintenance.geometric-repack.enabled false &&
1552+
git config set maintenance.cache-local-objects.enabled true &&
1553+
git config set maintenance.cache-local-objects.auto 1 &&
1554+
1555+
test_import_packfile &&
1556+
test_get_packdir_files "*.pack" "*.idx" "*.keep" "*.rev" \
1557+
>files.txt &&
1558+
test_get_loose_object_files >>files.txt &&
1559+
1560+
git maintenance run &&
1561+
while IFS= read -r f; do
1562+
test_path_exists $f || exit 1
1563+
done <files.txt
1564+
)
1565+
'
1566+
1567+
test_expect_success 'cache-local-objects task cache path same as local odb no op' '
1568+
test_when_finished "rm -rf repo" &&
1569+
git init repo &&
1570+
(
1571+
cd repo &&
1572+
1573+
test_commit something &&
1574+
git config set gvfs.sharedcache .git/objects &&
1575+
git config set maintenance.gc.enabled false &&
1576+
git config set maintenance.geometric-repack.enabled false &&
1577+
git config set maintenance.cache-local-objects.enabled true &&
1578+
git config set maintenance.cache-local-objects.auto 1 &&
1579+
1580+
test_import_packfile &&
1581+
test_get_packdir_files "*.pack" "*.idx" "*.keep" "*.rev" \
1582+
>files.txt &&
1583+
test_get_loose_object_files >>files.txt &&
1584+
1585+
git maintenance run &&
1586+
while IFS= read -r f; do
1587+
test_path_exists $f || exit 1
1588+
done <files.txt
1589+
)
1590+
'
1591+
1592+
test_expect_success 'cache-local-objects task no .rev or .keep' '
1593+
test_when_finished "rm -rf repo cache" &&
1594+
mkdir -p cache/pack &&
1595+
git init repo &&
1596+
(
1597+
cd repo &&
1598+
1599+
test_commit something &&
1600+
git config set gvfs.sharedcache ../cache &&
1601+
git config set maintenance.gc.enabled false &&
1602+
git config set maintenance.geometric-repack.enabled false &&
1603+
git config set maintenance.cache-local-objects.enabled true &&
1604+
git config set maintenance.cache-local-objects.auto 1 &&
1605+
1606+
test_import_packfile &&
1607+
test_get_packdir_files "*.pack" "*.idx" >src.txt &&
1608+
test_get_loose_object_files >>src.txt &&
1609+
1610+
rm -f .git/objects/pack/*.rev .git/objects/pack/*.keep &&
1611+
1612+
sed "s/.git\\/objects\\//..\\/cache\\//" src.txt >dst.txt &&
1613+
1614+
git maintenance run &&
1615+
while IFS= read -r f; do
1616+
test_path_is_missing $f || exit 1
1617+
done <src.txt &&
1618+
1619+
while IFS= read -r f; do
1620+
test_path_exists $f || exit 1
1621+
done <dst.txt
1622+
)
1623+
'
1624+
1625+
test_expect_success 'cache-local-objects task success' '
1626+
test_when_finished "rm -rf repo cache" &&
1627+
mkdir -p cache/pack &&
1628+
git init repo &&
1629+
(
1630+
cd repo &&
1631+
1632+
test_commit something &&
1633+
git config set gvfs.sharedcache ../cache &&
1634+
git config set maintenance.gc.enabled false &&
1635+
git config set maintenance.geometric-repack.enabled false &&
1636+
git config set maintenance.cache-local-objects.enabled true &&
1637+
git config set maintenance.cache-local-objects.auto 1 &&
1638+
1639+
test_import_packfile &&
1640+
test_get_packdir_files "*.pack" "*.idx" "*.keep" "*.rev" \
1641+
>src.txt &&
1642+
test_get_loose_object_files >>src.txt &&
1643+
1644+
sed "s/.git\\/objects\\//..\\/cache\\//" src.txt >dst.txt &&
1645+
1646+
git maintenance run &&
1647+
while IFS= read -r f; do
1648+
test_path_is_missing $f || exit 1
1649+
done <src.txt &&
1650+
1651+
while IFS= read -r f; do
1652+
test_path_exists $f || exit 1
1653+
done <dst.txt
1654+
)
1655+
'
1656+
15241657
test_done

0 commit comments

Comments
 (0)