reset: make --mixed sparse-aware

Remove the `ensure_full_index` guard on `read_from_tree` and update `git reset --mixed` to ensure it can use sparse directory index entries wherever possible. Sparse directory entries are reset using `diff_tree_oid`, which requires `change` and `add_remove` functions to process the internal contents of the sparse directory. The `recursive` diff option handles cases in which `reset --mixed` must diff/merge files that are nested multiple levels deep in a sparse directory. The use of pathspecs with `git reset --mixed` introduces scenarios in which internal contents of sparse directories may be matched by the pathspec. In order to reset *all* files in the repo that may match the pathspec, the following conditions on the pathspec require index expansion before performing the reset: * "magic" pathspecs * wildcard pathspecs that do not match only in-cone files or entire sparse directories * literal pathspecs matching something outside the sparse checkout definition Helped-by: Elijah Newren <newren@gmail.com> Signed-off-by: Victoria Dye <vdye@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Victoria Dye committed Nov 29, 2021 at 15:52 UTC 4d1cfc1351ffec47bba1318e9cd1ed13c5182951
2 files changed +102 -2
builtin/reset.c
+85 -2
@@ -148,7 +148,9 @@ static void update_index_from_diff(struct diff_queue_struct *q,
148 * If the file 1) corresponds to an existing index entry with
149 * skip-worktree set, or 2) does not exist in the index but is
150 * outside the sparse checkout definition, add a skip-worktree bit
151 - * to the new index entry.
151 + * to the new index entry. Note that a sparse index will be expanded
152 + * if this entry is outside the sparse cone - this is necessary
153 + * to properly construct the reset sparse directory.
154 */
155 pos = cache_name_pos(one->path, strlen(one->path));
156 if ((pos >= 0 && ce_skip_worktree(active_cache[pos])) ||
@@ -166,6 +168,82 @@ static void update_index_from_diff(struct diff_queue_struct *q,
168 }
169 }
170
171 +static int pathspec_needs_expanded_index(const struct pathspec *pathspec)
172 +{
173 + unsigned int i, pos;
174 + int res = 0;
175 + char *skip_worktree_seen = NULL;
176 +
177 + /*
178 + * When using a magic pathspec, assume for the sake of simplicity that
179 + * the index needs to be expanded to match all matchable files.
180 + */
181 + if (pathspec->magic)
182 + return 1;
183 +
184 + for (i = 0; i < pathspec->nr; i++) {
185 + struct pathspec_item item = pathspec->items[i];
186 +
187 + /*
188 + * If the pathspec item has a wildcard, the index should be expanded
189 + * if the pathspec has the possibility of matching a subset of entries inside
190 + * of a sparse directory (but not the entire directory).
191 + *
192 + * If the pathspec item is a literal path, the index only needs to be expanded
193 + * if a) the pathspec isn't in the sparse checkout cone (to make sure we don't
194 + * expand for in-cone files) and b) it doesn't match any sparse directories
195 + * (since we can reset whole sparse directories without expanding them).
196 + */
197 + if (item.nowildcard_len < item.len) {
198 + /*
199 + * Special case: if the pattern is a path inside the cone
200 + * followed by only wildcards, the pattern cannot match
201 + * partial sparse directories, so we don't expand the index.
202 + */
203 + if (path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
204 + strspn(item.original + item.nowildcard_len, "*") == item.len - item.nowildcard_len)
205 + continue;
206 +
207 + for (pos = 0; pos < active_nr; pos++) {
208 + struct cache_entry *ce = active_cache[pos];
209 +
210 + if (!S_ISSPARSEDIR(ce->ce_mode))
211 + continue;
212 +
213 + /*
214 + * If the pre-wildcard length is longer than the sparse
215 + * directory name and the sparse directory is the first
216 + * component of the pathspec, need to expand the index.
217 + */
218 + if (item.nowildcard_len > ce_namelen(ce) &&
219 + !strncmp(item.original, ce->name, ce_namelen(ce))) {
220 + res = 1;
221 + break;
222 + }
223 +
224 + /*
225 + * If the pre-wildcard length is shorter than the sparse
226 + * directory and the pathspec does not match the whole
227 + * directory, need to expand the index.
228 + */
229 + if (!strncmp(item.original, ce->name, item.nowildcard_len) &&
230 + wildmatch(item.original, ce->name, 0)) {
231 + res = 1;
232 + break;
233 + }
234 + }
235 + } else if (!path_in_cone_mode_sparse_checkout(item.original, &the_index) &&
236 + !matches_skip_worktree(pathspec, i, &skip_worktree_seen))
237 + res = 1;
238 +
239 + if (res > 0)
240 + break;
241 + }
242 +
243 + free(skip_worktree_seen);
244 + return res;
245 +}
246 +
247 static int read_from_tree(const struct pathspec *pathspec,
248 struct object_id *tree_oid,
249 int intent_to_add)
@@ -178,9 +256,14 @@ static int read_from_tree(const struct pathspec *pathspec,
256 opt.format_callback = update_index_from_diff;
257 opt.format_callback_data = &intent_to_add;
258 opt.flags.override_submodule_config = 1;
259 + opt.flags.recursive = 1;
260 opt.repo = the_repository;
261 + opt.change = diff_change;
262 + opt.add_remove = diff_addremove;
263 +
264 + if (pathspec->nr && the_index.sparse_index && pathspec_needs_expanded_index(pathspec))
265 + ensure_full_index(&the_index);
266
183 - ensure_full_index(&the_index);
267 if (do_diff_cache(tree_oid, &opt))
268 return 1;
269 diffcore_std(&opt);
t/t1092-sparse-checkout-compatibility.sh
+17
@@ -795,11 +795,28 @@ test_expect_success 'sparse-index is not expanded' '
795 ensure_not_expanded reset --hard $ref || return 1
796 done &&
797
798 + ensure_not_expanded reset --mixed base &&
799 ensure_not_expanded reset --hard update-deep &&
800 ensure_not_expanded reset --keep base &&
801 ensure_not_expanded reset --merge update-deep &&
802 ensure_not_expanded reset --hard &&
803
804 + ensure_not_expanded reset base -- deep/a &&
805 + ensure_not_expanded reset base -- nonexistent-file &&
806 + ensure_not_expanded reset deepest -- deep &&
807 +
808 + # Although folder1 is outside the sparse definition, it exists as a
809 + # directory entry in the index, so the pathspec will not force the
810 + # index to be expanded.
811 + ensure_not_expanded reset deepest -- folder1 &&
812 + ensure_not_expanded reset deepest -- folder1/ &&
813 +
814 + # Wildcard identifies only in-cone files, no index expansion
815 + ensure_not_expanded reset deepest -- deep/\* &&
816 +
817 + # Wildcard identifies only full sparse directories, no index expansion
818 + ensure_not_expanded reset deepest -- folder\* &&
819 +
820 ensure_not_expanded checkout -f update-deep &&
821 test_config -C sparse-index pull.twohead ort &&
822 (