Skip to content

Commit 52ac048

Browse files
authored
Merge pull request git-for-windows#426 from vdye/sparse-index/read-tree
Sparse index: integrate with `read-tree`
2 parents ce57f02 + 8220f59 commit 52ac048

File tree

8 files changed

+264
-31
lines changed

8 files changed

+264
-31
lines changed

builtin/read-tree.c

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,15 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix)
168168
argc = parse_options(argc, argv, cmd_prefix, read_tree_options,
169169
read_tree_usage, 0);
170170

171-
hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
172-
173171
prefix_set = opts.prefix ? 1 : 0;
174172
if (1 < opts.merge + opts.reset + prefix_set)
175173
die("Which one? -m, --reset, or --prefix?");
176174

175+
prepare_repo_settings(the_repository);
176+
the_repository->settings.command_requires_full_index = 0;
177+
178+
hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
179+
177180
/*
178181
* NEEDSWORK
179182
*
@@ -214,6 +217,9 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix)
214217
if (opts.merge && !opts.index_only)
215218
setup_work_tree();
216219

220+
if (opts.skip_sparse_checkout)
221+
ensure_full_index(&the_index);
222+
217223
if (opts.merge) {
218224
switch (stage - 1) {
219225
case 0:
@@ -223,11 +229,21 @@ int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix)
223229
opts.fn = opts.prefix ? bind_merge : oneway_merge;
224230
break;
225231
case 2:
232+
/*
233+
* TODO: update twoway_merge to handle edit/edit conflicts in
234+
* sparse directories.
235+
*/
236+
ensure_full_index(&the_index);
226237
opts.fn = twoway_merge;
227238
opts.initial_checkout = is_cache_unborn();
228239
break;
229240
case 3:
230241
default:
242+
/*
243+
* TODO: update threeway_merge to handle edit/edit conflicts in
244+
* sparse directories.
245+
*/
246+
ensure_full_index(&the_index);
231247
opts.fn = threeway_merge;
232248
break;
233249
}

cache-tree.c

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,17 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state,
800800
return ret;
801801
}
802802

803+
static void prime_cache_tree_sparse_dir(struct repository *r,
804+
struct cache_tree *it,
805+
struct tree *tree,
806+
struct strbuf *tree_path)
807+
{
808+
809+
oidcpy(&it->oid, &tree->object.oid);
810+
it->entry_count = 1;
811+
return;
812+
}
813+
803814
static void prime_cache_tree_rec(struct repository *r,
804815
struct cache_tree *it,
805816
struct tree *tree,
@@ -812,21 +823,6 @@ static void prime_cache_tree_rec(struct repository *r,
812823

813824
oidcpy(&it->oid, &tree->object.oid);
814825

815-
/*
816-
* If this entry is outside the sparse-checkout cone, then it might be
817-
* a sparse directory entry. Check the index to ensure it is by looking
818-
* for an entry with the exact same name as the tree. If no matching sparse
819-
* entry is found, a staged or conflicted entry is preventing this
820-
* directory from collapsing to a sparse directory entry, so the cache
821-
* tree expansion should continue.
822-
*/
823-
if (r->index->sparse_index &&
824-
!path_in_cone_modesparse_checkout(tree_path->buf, r->index) &&
825-
index_name_pos(r->index, tree_path->buf, tree_path->len) >= 0) {
826-
it->entry_count = 1;
827-
return;
828-
}
829-
830826
init_tree_desc(&desc, tree->buffer, tree->size);
831827
cnt = 0;
832828
while (tree_entry(&desc, &entry)) {
@@ -846,7 +842,18 @@ static void prime_cache_tree_rec(struct repository *r,
846842
strbuf_add(&subtree_path, entry.path, entry.pathlen);
847843
strbuf_addch(&subtree_path, '/');
848844

849-
prime_cache_tree_rec(r, sub->cache_tree, subtree, &subtree_path);
845+
/*
846+
* If a sparse index is in use, the directory being processed may be
847+
* sparse. To confirm that, we can check whether an entry with that
848+
* exact name exists in the index. If it does, the created subtree
849+
* should be sparse. Otherwise, cache tree expansion should continue
850+
* as normal.
851+
*/
852+
if (r->index->sparse_index &&
853+
index_entry_exists(r->index, subtree_path.buf, subtree_path.len))
854+
prime_cache_tree_sparse_dir(r, sub->cache_tree, subtree, &subtree_path);
855+
else
856+
prime_cache_tree_rec(r, sub->cache_tree, subtree, &subtree_path);
850857
cnt += sub->cache_tree->entry_count;
851858
}
852859
}

cache.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,16 @@ struct cache_entry *index_file_next_match(struct index_state *istate, struct cac
833833
*/
834834
int index_name_pos(struct index_state *, const char *name, int namelen);
835835

836+
/*
837+
* Determines whether an entry with the given name exists within the
838+
* given index. The return value is 1 if an exact match is found, otherwise
839+
* it is 0. Note that, unlike index_name_pos, this function does not expand
840+
* the index if it is sparse. If an item exists within the full index but it
841+
* is contained within a sparse directory (and not in the sparse index), 0 is
842+
* returned.
843+
*/
844+
int index_entry_exists(struct index_state *, const char *name, int namelen);
845+
836846
/*
837847
* Some functions return the negative complement of an insert position when a
838848
* precise match was not found but a position was found where the entry would

read-cache.c

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,10 @@ int cache_name_stage_compare(const char *name1, int len1, int stage1, const char
564564
return 0;
565565
}
566566

567-
static int index_name_stage_pos(struct index_state *istate, const char *name, int namelen, int stage)
567+
static int index_name_stage_pos(struct index_state *istate,
568+
const char *name, int namelen,
569+
int stage,
570+
int search_sparse)
568571
{
569572
int first, last;
570573

@@ -583,7 +586,7 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in
583586
first = next+1;
584587
}
585588

586-
if (istate->sparse_index &&
589+
if (search_sparse && istate->sparse_index &&
587590
first > 0) {
588591
/* Note: first <= istate->cache_nr */
589592
struct cache_entry *ce = istate->cache[first - 1];
@@ -599,7 +602,7 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in
599602
ce_namelen(ce) < namelen &&
600603
!strncmp(name, ce->name, ce_namelen(ce))) {
601604
ensure_full_index(istate);
602-
return index_name_stage_pos(istate, name, namelen, stage);
605+
return index_name_stage_pos(istate, name, namelen, stage, search_sparse);
603606
}
604607
}
605608

@@ -608,7 +611,12 @@ static int index_name_stage_pos(struct index_state *istate, const char *name, in
608611

609612
int index_name_pos(struct index_state *istate, const char *name, int namelen)
610613
{
611-
return index_name_stage_pos(istate, name, namelen, 0);
614+
return index_name_stage_pos(istate, name, namelen, 0, 1);
615+
}
616+
617+
int index_entry_exists(struct index_state *istate, const char *name, int namelen)
618+
{
619+
return index_name_stage_pos(istate, name, namelen, 0, 0) >= 0;
612620
}
613621

614622
int remove_index_entry_at(struct index_state *istate, int pos)
@@ -1235,7 +1243,7 @@ static int has_dir_name(struct index_state *istate,
12351243
*/
12361244
}
12371245

1238-
pos = index_name_stage_pos(istate, name, len, stage);
1246+
pos = index_name_stage_pos(istate, name, len, stage, 1);
12391247
if (pos >= 0) {
12401248
/*
12411249
* Found one, but not so fast. This could
@@ -1332,7 +1340,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e
13321340
strcmp(ce->name, istate->cache[istate->cache_nr - 1]->name) > 0)
13331341
pos = index_pos_to_insert_pos(istate->cache_nr);
13341342
else
1335-
pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce));
1343+
pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce), 1);
13361344

13371345
/*
13381346
* Cache tree path should be invalidated only after index_name_stage_pos,
@@ -1374,7 +1382,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e
13741382
if (!ok_to_replace)
13751383
return error(_("'%s' appears as both a file and as a directory"),
13761384
ce->name);
1377-
pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce));
1385+
pos = index_name_stage_pos(istate, ce->name, ce_namelen(ce), ce_stage(ce), 1);
13781386
pos = -pos-1;
13791387
}
13801388
return pos + 1;

t/perf/p2000-sparse-operations.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ test_perf_on_all git commit -a -m A
112112
test_perf_on_all git checkout -f -
113113
test_perf_on_all git reset
114114
test_perf_on_all git reset --hard
115+
test_perf_on_all git read-tree -mu HEAD
115116
test_perf_on_all git checkout-index -f --all
116117
test_perf_on_all git update-index --add --remove
117118
test_perf_on_all git diff

t/t1092-sparse-checkout-compatibility.sh

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,22 +390,26 @@ test_expect_success 'diff --staged' '
390390
test_expect_success 'diff partially-staged' '
391391
init_repos &&
392392
393+
write_script edit-contents <<-\EOF &&
394+
echo text >>$1
395+
EOF
396+
393397
# Add file within cone
394398
test_all_match git sparse-checkout set deep &&
395-
run_on_all 'echo >deep/testfile' &&
399+
run_on_all ../edit-contents deep/testfile &&
396400
test_all_match git add deep/testfile &&
397-
run_on_all 'echo a new line >>deep/testfile' &&
401+
run_on_all ../edit-contents deep/testfile &&
398402
399403
test_all_match git diff &&
400404
test_all_match git diff --staged &&
401405
402406
# Add file outside cone
403407
test_all_match git reset --hard &&
404408
run_on_all mkdir newdirectory &&
405-
run_on_all 'echo >newdirectory/testfile' &&
409+
run_on_all ../edit-contents newdirectory/testfile &&
406410
test_all_match git sparse-checkout set newdirectory &&
407411
test_all_match git add newdirectory/testfile &&
408-
run_on_all 'echo a new line >>newdirectory/testfile' &&
412+
run_on_all ../edit-contents newdirectory/testfile &&
409413
test_all_match git sparse-checkout set &&
410414
411415
test_all_match git diff &&
@@ -771,6 +775,117 @@ test_expect_success 'update-index --cacheinfo' '
771775
test_sparse_match git status --porcelain=v2
772776
'
773777

778+
test_expect_success 'read-tree --merge with files outside sparse definition' '
779+
init_repos &&
780+
781+
test_all_match git checkout -b test-branch update-folder1 &&
782+
for MERGE_TREES in "base HEAD update-folder2" \
783+
"update-folder1 update-folder2" \
784+
"update-folder2"
785+
do
786+
# Clean up and remove on-disk files
787+
test_all_match git reset --hard HEAD &&
788+
test_sparse_match git sparse-checkout reapply &&
789+
790+
# Although the index matches, without --no-sparse-checkout, outside-of-
791+
# definition files will not exist on disk for sparse checkouts
792+
test_all_match git read-tree -mu $MERGE_TREES &&
793+
test_all_match git status --porcelain=v2 &&
794+
test_path_is_missing sparse-checkout/folder2 &&
795+
test_path_is_missing sparse-index/folder2 &&
796+
797+
test_all_match git read-tree --reset -u HEAD &&
798+
test_all_match git status --porcelain=v2 &&
799+
800+
test_all_match git read-tree -mu --no-sparse-checkout $MERGE_TREES &&
801+
test_all_match git status --porcelain=v2 &&
802+
test_cmp sparse-checkout/folder2/a sparse-index/folder2/a &&
803+
test_cmp sparse-checkout/folder2/a full-checkout/folder2/a || return 1
804+
done
805+
'
806+
807+
test_expect_success 'read-tree --merge with edit/edit conflicts in sparse directories' '
808+
init_repos &&
809+
810+
# Merge of multiple changes to same directory (but not same files) should
811+
# succeed
812+
test_all_match git read-tree -mu base rename-base update-folder1 &&
813+
test_all_match git status --porcelain=v2 &&
814+
815+
test_all_match git reset --hard &&
816+
817+
test_all_match git read-tree -mu rename-base update-folder2 &&
818+
test_all_match git status --porcelain=v2 &&
819+
820+
test_all_match git reset --hard &&
821+
822+
test_all_match test_must_fail git read-tree -mu base update-folder1 rename-out-to-in &&
823+
test_all_match test_must_fail git read-tree -mu rename-out-to-in update-folder1
824+
'
825+
826+
test_expect_success 'read-tree --merge with modified file outside definition' '
827+
init_repos &&
828+
829+
write_script edit-contents <<-\EOF &&
830+
echo text >>$1
831+
EOF
832+
833+
test_all_match git checkout -b test-branch update-folder1 &&
834+
run_on_sparse mkdir -p folder2 &&
835+
run_on_all ../edit-contents folder2/a &&
836+
837+
# With manually-modified file, full-checkout cannot merge, but it is ignored
838+
# in sparse checkouts
839+
test_must_fail git -C full-checkout read-tree -mu update-folder2 &&
840+
test_sparse_match git read-tree -mu update-folder2 &&
841+
test_sparse_match git status --porcelain=v2 &&
842+
843+
# Reset only the sparse checkouts to "undo" the merge. All three checkouts
844+
# now have matching indexes and matching folder2/a on disk.
845+
test_sparse_match git read-tree --reset -u HEAD &&
846+
847+
# When --no-sparse-checkout is specified, sparse checkouts identify the file
848+
# on disk and prevent the merge
849+
test_all_match test_must_fail git read-tree -mu --no-sparse-checkout update-folder2
850+
'
851+
852+
test_expect_success 'read-tree --prefix outside sparse definition' '
853+
init_repos &&
854+
855+
# Cannot read-tree --prefix with a single argument when files exist within
856+
# prefix
857+
test_all_match test_must_fail git read-tree --prefix=folder1/ -u update-folder1 &&
858+
859+
test_all_match git read-tree --prefix=folder2/0 -u rename-base &&
860+
test_path_is_missing sparse-checkout/folder2 &&
861+
test_path_is_missing sparse-index/folder2 &&
862+
863+
test_all_match git read-tree --reset -u HEAD &&
864+
test_all_match git read-tree --prefix=folder2/0 -u --no-sparse-checkout rename-base &&
865+
test_cmp sparse-checkout/folder2/0/a sparse-index/folder2/0/a &&
866+
test_cmp sparse-checkout/folder2/0/a full-checkout/folder2/0/a
867+
'
868+
869+
test_expect_success 'read-tree --merge with directory-file conflicts' '
870+
init_repos &&
871+
872+
test_all_match git checkout -b test-branch rename-base &&
873+
874+
# Although the index matches, without --no-sparse-checkout, outside-of-
875+
# definition files will not exist on disk for sparse checkouts
876+
test_sparse_match git read-tree -mu rename-out-to-out &&
877+
test_sparse_match git status --porcelain=v2 &&
878+
test_path_is_missing sparse-checkout/folder2 &&
879+
test_path_is_missing sparse-index/folder2 &&
880+
881+
test_sparse_match git read-tree --reset -u HEAD &&
882+
test_sparse_match git status --porcelain=v2 &&
883+
884+
test_sparse_match git read-tree -mu --no-sparse-checkout rename-out-to-out &&
885+
test_sparse_match git status --porcelain=v2 &&
886+
test_cmp sparse-checkout/folder2/0/1 sparse-index/folder2/0/1
887+
'
888+
774889
test_expect_success 'merge, cherry-pick, and rebase' '
775890
init_repos &&
776891
@@ -995,7 +1110,7 @@ test_expect_success 'sparse-index is expanded and converted back' '
9951110
init_repos &&
9961111
9971112
GIT_TRACE2_EVENT="$(pwd)/trace2.txt" GIT_TRACE2_EVENT_NESTING=10 \
998-
git -C sparse-index -c core.fsmonitor="" read-tree -mu HEAD &&
1113+
git -C sparse-index -c core.fsmonitor="" mv a b &&
9991114
test_region index convert_to_sparse trace2.txt &&
10001115
test_region index ensure_full_index trace2.txt
10011116
'
@@ -1050,7 +1165,7 @@ test_expect_success 'sparse-index is not expanded' '
10501165
do
10511166
echo >>sparse-index/README.md &&
10521167
ensure_not_expanded reset --mixed $ref
1053-
ensure_not_expanded reset --hard $ref
1168+
ensure_not_expanded reset --hard $ref || return 1
10541169
done &&
10551170
10561171
ensure_not_expanded reset --hard update-deep &&
@@ -1135,6 +1250,23 @@ test_expect_success 'sparse index is not expanded: update-index' '
11351250
ensure_not_expanded update-index --add --remove --again
11361251
'
11371252

1253+
test_expect_success 'sparse index is not expanded: read-tree' '
1254+
init_repos &&
1255+
1256+
ensure_not_expanded checkout -b test-branch update-folder1 &&
1257+
for MERGE_TREES in "update-folder2"
1258+
do
1259+
ensure_not_expanded read-tree -mu $MERGE_TREES &&
1260+
ensure_not_expanded reset --hard HEAD || return 1
1261+
done &&
1262+
1263+
rm -rf sparse-index/deep/deeper2 &&
1264+
ensure_not_expanded add . &&
1265+
ensure_not_expanded commit -m "test" &&
1266+
1267+
ensure_not_expanded read-tree --prefix=deep/deeper2 -u deepest
1268+
'
1269+
11381270
# NEEDSWORK: a sparse-checkout behaves differently from a full checkout
11391271
# in this scenario, but it shouldn't.
11401272
test_expect_success 'reset mixed and checkout orphan' '

0 commit comments

Comments
 (0)