Skip to content

Commit

Permalink
unpack-trees: handle missing sparse directories
Browse files Browse the repository at this point in the history
If a sparse directory does not exist in the index, unpack it at the
directory level rather than recursing into it an unpacking its contents
file-by-file. This helps keep the sparse index as collapsed as possible in
cases such as 'git reset --hard' restoring a sparse directory.

A directory is determined to be truly non-existent in the index (rather than
the parent of existing index entries), if 1) its path is outside the sparse
cone and 2) there are no children of the directory in the index. This check
is performed by 'missing_dir_is_sparse()' in 'unpack_single_entry()'. If the
directory is a missing sparse dir, 'unpack_single_entry()'  will proceed
with unpacking it. This determination is also propagated back up to
'unpack_callback()' via 'is_missing_sparse_dir' to prevent further tree
traversal into the unpacked directory.

Reported-by: Shaoxuan Yuan <shaoxuan.yuan02@gmail.com>
Signed-off-by: Victoria Dye <vdye@github.com>
  • Loading branch information
vdye committed Aug 4, 2022
1 parent f7978d2 commit 016971a
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 10 deletions.
17 changes: 17 additions & 0 deletions t/t1092-sparse-checkout-compatibility.sh
Expand Up @@ -695,6 +695,23 @@ test_expect_success 'reset with wildcard pathspec' '
test_all_match git ls-files -s -- folder1
'

test_expect_success 'reset hard with removed sparse dir' '
init_repos &&
test_all_match git rm -r --sparse folder1 &&
test_all_match git status --porcelain=v2 &&
test_all_match git reset --hard &&
test_all_match git status --porcelain=v2 &&
cat >expect <<-\EOF &&
folder1/
EOF
git -C sparse-index ls-files --sparse folder1 >out &&
test_cmp expect out
'

test_expect_success 'update-index modify outside sparse definition' '
init_repos &&
Expand Down
88 changes: 78 additions & 10 deletions unpack-trees.c
Expand Up @@ -1069,6 +1069,53 @@ static struct cache_entry *create_ce_entry(const struct traverse_info *info,
return ce;
}

/*
* Determine whether the path specified corresponds to a sparse directory
* completely missing from the index. This function is assumed to only be
* called when the named path isn't already in the index.
*/
static int missing_dir_is_sparse(const struct traverse_info *info,
const struct name_entry *p)
{
int res, pos;
struct strbuf dirpath = STRBUF_INIT;
struct unpack_trees_options *o = info->data;

/*
* First, check whether the path is in the sparse cone. If it is,
* then this directory shouldn't be sparse.
*/
strbuf_add(&dirpath, info->traverse_path, info->pathlen);
strbuf_add(&dirpath, p->path, p->pathlen);
strbuf_addch(&dirpath, '/');
if (path_in_cone_mode_sparse_checkout(dirpath.buf, o->src_index)) {
res = 0;
goto cleanup;
}

/*
* Given that the directory is not inside the sparse cone, it could be
* (partially) expanded in the index. If child entries exist, the path
* is not a missing sparse directory.
*/
pos = index_name_pos_sparse(o->src_index, dirpath.buf, dirpath.len);
if (pos >= 0)
BUG("cache entry '%s%s' shouldn't exist in the index",
info->traverse_path, p->path);

pos = -pos - 1;
if (pos >= o->src_index->cache_nr) {
res = 1;
goto cleanup;
}

res = strncmp(o->src_index->cache[pos]->name, dirpath.buf, dirpath.len);

cleanup:
strbuf_release(&dirpath);
return res;
}

/*
* Note that traverse_by_cache_tree() duplicates some logic in this function
* without actually calling it. If you change the logic here you may need to
Expand All @@ -1078,21 +1125,40 @@ static int unpack_single_entry(int n, unsigned long mask,
unsigned long dirmask,
struct cache_entry **src,
const struct name_entry *names,
const struct traverse_info *info)
const struct traverse_info *info,
int *is_missing_sparse_dir)
{
int i;
struct unpack_trees_options *o = info->data;
unsigned long conflicts = info->df_conflicts | dirmask;
const struct name_entry *p = names;

if (mask == dirmask && !src[0])
return 0;
*is_missing_sparse_dir = 0;
if (mask == dirmask && !src[0]) {
/*
* If the directory is completely missing from the index but
* would otherwise be a sparse directory, we should unpack it.
* If not, we'll return and continue recursively traversing the
* tree.
*/
if (!o->src_index->sparse_index)
return 0;

/* Find first entry with a real name (we could use "mask" too) */
while (!p->mode)
p++;

*is_missing_sparse_dir = missing_dir_is_sparse(info, p);
if (!*is_missing_sparse_dir)
return 0;
}

/*
* When we have a sparse directory entry for src[0],
* then this isn't necessarily a directory-file conflict.
* When we are unpacking a sparse directory, then this isn't necessarily
* a directory-file conflict.
*/
if (mask == dirmask && src[0] &&
S_ISSPARSEDIR(src[0]->ce_mode))
if (mask == dirmask &&
(*is_missing_sparse_dir || (src[0] && S_ISSPARSEDIR(src[0]->ce_mode))))
conflicts = 0;

/*
Expand Down Expand Up @@ -1352,7 +1418,7 @@ static int unpack_sparse_callback(int n, unsigned long mask, unsigned long dirma
{
struct cache_entry *src[MAX_UNPACK_TREES + 1] = { NULL, };
struct unpack_trees_options *o = info->data;
int ret;
int ret, is_missing_sparse_dir;

assert(o->merge);

Expand All @@ -1376,7 +1442,7 @@ static int unpack_sparse_callback(int n, unsigned long mask, unsigned long dirma
* "index" tree (i.e., names[0]) and adjust 'names', 'n', 'mask', and
* 'dirmask' accordingly.
*/
ret = unpack_single_entry(n - 1, mask >> 1, dirmask >> 1, src, names + 1, info);
ret = unpack_single_entry(n - 1, mask >> 1, dirmask >> 1, src, names + 1, info, &is_missing_sparse_dir);

if (src[0])
discard_cache_entry(src[0]);
Expand All @@ -1394,6 +1460,7 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str
struct cache_entry *src[MAX_UNPACK_TREES + 1] = { NULL, };
struct unpack_trees_options *o = info->data;
const struct name_entry *p = names;
int is_missing_sparse_dir;

/* Find first entry with a real name (we could use "mask" too) */
while (!p->mode)
Expand Down Expand Up @@ -1440,7 +1507,7 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str
}
}

if (unpack_single_entry(n, mask, dirmask, src, names, info) < 0)
if (unpack_single_entry(n, mask, dirmask, src, names, info, &is_missing_sparse_dir))
return -1;

if (o->merge && src[0]) {
Expand Down Expand Up @@ -1478,6 +1545,7 @@ static int unpack_callback(int n, unsigned long mask, unsigned long dirmask, str
}

if (!is_sparse_directory_entry(src[0], names, info) &&
!is_missing_sparse_dir &&
traverse_trees_recursive(n, dirmask, mask & ~dirmask,
names, info) < 0) {
return -1;
Expand Down

0 comments on commit 016971a

Please sign in to comment.