add: implement the --sparse option

We previously modified 'git add' to refuse updating index entries outside of the sparse-checkout cone. This is justified to prevent users from accidentally getting into a confusing state when Git removes those files from the working tree at some later point. Unfortunately, this caused some workflows that were previously possible to become impossible, especially around merge conflicts outside of the sparse-checkout cone. These were documented in tests within t1092. We now re-enable these workflows using a new '--sparse' option to 'git add'. This allows users to signal "Yes, I do know what I'm doing with these files," and accept the consequences of the files leaving the worktree later. We delay updating the advice message until implementing a similar option in 'git rm' and 'git mv'. Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Sep 24, 2021 at 15:39 UTC 0299a69694fcf486a0880439bb4dacfafe3ffc38
4 files changed +43 -24
Documentation/git-add.txt
+8 -1
@@ -9,7 +9,7 @@ SYNOPSIS
9 --------
10 [verse]
11 'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
12 - [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]]
12 + [--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]] [--sparse]
13 [--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
14 [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
15 [--] [<pathspec>...]
@@ -79,6 +79,13 @@ in linkgit:gitglossary[7].
79 --force::
80 Allow adding otherwise ignored files.
81
82 +--sparse::
83 + Allow updating index entries outside of the sparse-checkout cone.
84 + Normally, `git add` refuses to update index entries whose paths do
85 + not fit within the sparse-checkout cone, since those files might
86 + be removed from the working tree without warning. See
87 + linkgit:git-sparse-checkout[1] for more details.
88 +
89 -i::
90 --interactive::
91 Add modified contents in the working tree interactively to
builtin/add.c
+8 -4
@@ -30,6 +30,7 @@ static int patch_interactive, add_interactive, edit_interactive;
30 static int take_worktree_changes;
31 static int add_renormalize;
32 static int pathspec_file_nul;
33 +static int include_sparse;
34 static const char *pathspec_from_file;
35 static int legacy_stash_p; /* support for the scripted `git stash` */
36
@@ -46,7 +47,7 @@ static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only)
47 struct cache_entry *ce = active_cache[i];
48 int err;
49
49 - if (ce_skip_worktree(ce))
50 + if (!include_sparse && ce_skip_worktree(ce))
51 continue;
52
53 if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL))
@@ -95,7 +96,7 @@ static void update_callback(struct diff_queue_struct *q,
96 struct diff_filepair *p = q->queue[i];
97 const char *path = p->one->path;
98
98 - if (!path_in_sparse_checkout(path, &the_index))
99 + if (!include_sparse && !path_in_sparse_checkout(path, &the_index))
100 continue;
101
102 switch (fix_unmerged_status(p, data)) {
@@ -383,6 +384,7 @@ static struct option builtin_add_options[] = {
384 OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
385 OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
386 OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
387 + OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
388 OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
389 N_("override the executable bit of the listed files")),
390 OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
@@ -461,7 +463,8 @@ static int add_files(struct dir_struct *dir, int flags)
463 }
464
465 for (i = 0; i < dir->nr; i++) {
464 - if (!path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
466 + if (!include_sparse &&
467 + !path_in_sparse_checkout(dir->entries[i]->name, &the_index)) {
468 string_list_append(&matched_sparse_paths,
469 dir->entries[i]->name);
470 continue;
@@ -646,7 +649,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)
649 if (seen[i])
650 continue;
651
649 - if (matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) {
652 + if (!include_sparse &&
653 + matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) {
654 string_list_append(&only_match_skip_worktree,
655 pathspec.items[i].original);
656 continue;
t/t1092-sparse-checkout-compatibility.sh
+11 -18
@@ -343,11 +343,7 @@ test_expect_success 'commit including unstaged changes' '
343 test_all_match git status --porcelain=v2
344 '
345
346 -# NEEDSWORK: Now that 'git add folder1/new' fails, the changes being
347 -# attempted here fail for the sparse-checkout and sparse-index repos.
348 -# We must enable a way for adding files outside the sparse-checkout
349 -# done, even if it is by an optional flag.
350 -test_expect_failure 'status/add: outside sparse cone' '
346 +test_expect_success 'status/add: outside sparse cone' '
347 init_repos &&
348
349 # folder1 is at HEAD, but outside the sparse cone
@@ -375,15 +371,16 @@ test_expect_failure 'status/add: outside sparse cone' '
371 test_sparse_match test_must_fail git add folder1/new &&
372 grep "Disable or modify the sparsity rules" sparse-checkout-err &&
373 test_sparse_unstaged folder1/new &&
374 + test_sparse_match git add --sparse folder1/a &&
375 + test_sparse_match git add --sparse folder1/new &&
376
379 - # NEEDSWORK: behavior begins to deviate here.
380 - test_all_match git add . &&
377 + test_all_match git add --sparse . &&
378 test_all_match git status --porcelain=v2 &&
379 test_all_match git commit -m folder1/new &&
380 test_all_match git rev-parse HEAD^{tree} &&
381
382 run_on_all ../edit-contents folder1/newer &&
386 - test_all_match git add folder1/ &&
383 + test_all_match git add --sparse folder1/ &&
384 test_all_match git status --porcelain=v2 &&
385 test_all_match git commit -m folder1/newer &&
386 test_all_match git rev-parse HEAD^{tree}
@@ -527,12 +524,7 @@ test_expect_success 'merge, cherry-pick, and rebase' '
524 done
525 '
526
530 -# NEEDSWORK: This test is documenting current behavior, but that
531 -# behavior can be confusing to users so there is desire to change it.
532 -# Right now, users might be using this flow to work through conflicts,
533 -# so any solution should present advice to users who try this sequence
534 -# of commands to follow whatever new method we create.
535 -test_expect_failure 'merge with conflict outside cone' '
527 +test_expect_success 'merge with conflict outside cone' '
528 init_repos &&
529
530 test_all_match git checkout -b merge-tip merge-left &&
@@ -549,17 +541,16 @@ test_expect_failure 'merge with conflict outside cone' '
541 test_sparse_match test_must_fail git add folder1/a &&
542 grep "Disable or modify the sparsity rules" sparse-checkout-err &&
543 test_sparse_unstaged folder1/a &&
544 + test_all_match git add --sparse folder1/a &&
545 test_all_match git status --porcelain=v2 &&
546
547 # 3. Rename the file to another sparse filename and
548 # accept conflict markers as resolved content.
549 run_on_all mv folder2/a folder2/z &&
557 - # NEEDSWORK: This mode now fails, because folder2/z is
558 - # outside of the sparse-checkout cone and does not match an
559 - # existing index entry with the SKIP_WORKTREE bit cleared.
550 test_sparse_match test_must_fail git add folder2 &&
551 grep "Disable or modify the sparsity rules" sparse-checkout-err &&
552 test_sparse_unstaged folder2/z &&
553 + test_all_match git add --sparse folder2 &&
554 test_all_match git status --porcelain=v2 &&
555
556 test_all_match git merge --continue &&
@@ -567,7 +558,7 @@ test_expect_failure 'merge with conflict outside cone' '
558 test_all_match git rev-parse HEAD^{tree}
559 '
560
570 -test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
561 +test_expect_success 'cherry-pick/rebase with conflict outside cone' '
562 init_repos &&
563
564 for OPERATION in cherry-pick rebase
@@ -590,6 +581,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
581 test_sparse_match test_must_fail git add folder1/a &&
582 grep "Disable or modify the sparsity rules" sparse-checkout-err &&
583 test_sparse_unstaged folder1/a &&
584 + test_all_match git add --sparse folder1/a &&
585 test_all_match git status --porcelain=v2 &&
586
587 # 3. Rename the file to another sparse filename and
@@ -601,6 +593,7 @@ test_expect_failure 'cherry-pick/rebase with conflict outside cone' '
593 test_sparse_match test_must_fail git add folder2 &&
594 grep "Disable or modify the sparsity rules" sparse-checkout-err &&
595 test_sparse_unstaged folder2/z &&
596 + test_all_match git add --sparse folder2 &&
597 test_all_match git status --porcelain=v2 &&
598
599 test_all_match git $OPERATION --continue &&
t/t3705-add-sparse-checkout.sh
+16 -1
@@ -167,7 +167,13 @@ test_expect_success 'git add fails outside of sparse-checkout definition' '
167
168 git update-index --no-skip-worktree sparse_entry &&
169 test_must_fail git add sparse_entry &&
170 - test_sparse_entry_unstaged
170 + test_sparse_entry_unstaged &&
171 +
172 + # Avoid munging CRLFs to avoid an error message
173 + git -c core.autocrlf=input add --sparse sparse_entry 2>stderr &&
174 + test_must_be_empty stderr &&
175 + test-tool read-cache --table >actual &&
176 + grep "^100644 blob.*sparse_entry\$" actual
177 '
178
179 test_expect_success 'add obeys advice.updateSparsePath' '
@@ -178,4 +184,13 @@ test_expect_success 'add obeys advice.updateSparsePath' '
184
185 '
186
187 +test_expect_success 'add allows sparse entries with --sparse' '
188 + git sparse-checkout set a &&
189 + echo modified >sparse_entry &&
190 + test_must_fail git add sparse_entry &&
191 + test_sparse_entry_unchanged &&
192 + git add --sparse sparse_entry 2>stderr &&
193 + test_must_be_empty stderr
194 +'
195 +
196 test_done