Skip to content

Commit c27a17a

Browse files
committed
sparse-checkout: write escaped patterns in cone mode
If a user somehow creates a directory with an asterisk (*) or backslash (\), then the "git sparse-checkout set" command will struggle to provide the correct pattern in the sparse-checkout file. When not in cone mode, the provided pattern is written directly into the sparse-checkout file. However, in cone mode we expect a list of paths to directories and then we convert those into patterns. Even more specifically, the goal is to always allow the following from the root of a repo: git ls-tree --name-only -d HEAD | git sparse-checkout set --stdin The ls-tree command provides directory names with an unescaped asterisk. It also quotes the directories that contain an escaped backslash. We must remove these quotes, then keep the escaped backslashes. However, there is some care needed for the timing of these escapes. The in-memory pattern list is used to update the working directory before writing the patterns to disk. Thus, we need the command to have the unescaped names in the hashsets for the cone comparisons, then escape the patterns later. Signed-off-by: Derrick Stolee <[email protected]>
1 parent 65c53d7 commit c27a17a

File tree

2 files changed

+64
-5
lines changed

2 files changed

+64
-5
lines changed

builtin/sparse-checkout.c

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ static int update_working_directory(struct pattern_list *pl)
140140
return result;
141141
}
142142

143+
static char *escaped_pattern(char *pattern)
144+
{
145+
char *p = pattern;
146+
struct strbuf final = STRBUF_INIT;
147+
148+
while (*p) {
149+
if (*p == '*' || *p == '\\')
150+
strbuf_addch(&final, '\\');
151+
152+
strbuf_addch(&final, *p);
153+
p++;
154+
}
155+
156+
return strbuf_detach(&final, NULL);
157+
}
158+
143159
static void write_cone_to_file(FILE *fp, struct pattern_list *pl)
144160
{
145161
int i;
@@ -164,10 +180,11 @@ static void write_cone_to_file(FILE *fp, struct pattern_list *pl)
164180
fprintf(fp, "/*\n!/*/\n");
165181

166182
for (i = 0; i < sl.nr; i++) {
167-
char *pattern = sl.items[i].string;
183+
char *pattern = escaped_pattern(sl.items[i].string);
168184

169185
if (strlen(pattern))
170186
fprintf(fp, "%s/\n!%s/*/\n", pattern, pattern);
187+
free(pattern);
171188
}
172189

173190
string_list_clear(&sl, 0);
@@ -185,8 +202,9 @@ static void write_cone_to_file(FILE *fp, struct pattern_list *pl)
185202
string_list_remove_duplicates(&sl, 0);
186203

187204
for (i = 0; i < sl.nr; i++) {
188-
char *pattern = sl.items[i].string;
205+
char *pattern = escaped_pattern(sl.items[i].string);
189206
fprintf(fp, "%s/\n", pattern);
207+
free(pattern);
190208
}
191209
}
192210

@@ -337,7 +355,9 @@ static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *pat
337355
{
338356
struct pattern_entry *e = xmalloc(sizeof(*e));
339357
e->patternlen = path->len;
340-
e->pattern = strbuf_detach(path, NULL);
358+
e->pattern = dup_and_filter_pattern(path->buf);
359+
strbuf_release(path);
360+
341361
hashmap_entry_init(&e->ent,
342362
ignore_case ?
343363
strihash(e->pattern) :
@@ -369,13 +389,35 @@ static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *pat
369389

370390
static void strbuf_to_cone_pattern(struct strbuf *line, struct pattern_list *pl)
371391
{
392+
int i;
372393
strbuf_trim(line);
373394

374395
strbuf_trim_trailing_dir_sep(line);
375396

376397
if (!line->len)
377398
return;
378399

400+
for (i = 0; i < line->len; i++) {
401+
if (line->buf[i] == '*') {
402+
strbuf_insert(line, i, "\\", 1);
403+
i++;
404+
}
405+
406+
if (line->buf[i] == '\\') {
407+
if (i < line->len - 1 && line->buf[i + 1] == '\\')
408+
i++;
409+
else
410+
strbuf_insert(line, i, "\\", 1);
411+
412+
i++;
413+
}
414+
}
415+
416+
if (line->buf[0] == '"' && line->buf[line->len - 1] == '"') {
417+
strbuf_remove(line, 0, 1);
418+
strbuf_remove(line, line->len - 1, 1);
419+
}
420+
379421
if (line->buf[0] != '/')
380422
strbuf_insert(line, 0, "/", 1);
381423

t/t1091-sparse-checkout-builtin.sh

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ check_read_tree_errors () {
309309
REPO=$1
310310
FILES=$2
311311
ERRORS=$3
312+
git -C $REPO -c core.sparseCheckoutCone=false read-tree -mu HEAD 2>err &&
313+
test_must_be_empty err &&
314+
check_files $REPO "$FILES" &&
312315
git -C $REPO read-tree -mu HEAD 2>err &&
313316
if test -z "$ERRORS"
314317
then
@@ -379,14 +382,28 @@ test_expect_success BSLASHPSPEC 'pattern-checks: escaped "*"' '
379382
git -C escaped reset --hard $COMMIT &&
380383
check_files escaped "a deep folder1 folder2 zbad\\dir zdoes*exist" &&
381384
git -C escaped sparse-checkout init --cone &&
382-
cat >escaped/.git/info/sparse-checkout <<-\EOF &&
385+
git -C escaped sparse-checkout set zbad\\dir zdoes\*not\*exist zdoes\*exist &&
386+
cat >expect <<-\EOF &&
383387
/*
384388
!/*/
385389
/zbad\\dir/
390+
/zdoes\*exist/
386391
/zdoes\*not\*exist/
392+
EOF
393+
test_cmp expect escaped/.git/info/sparse-checkout &&
394+
check_read_tree_errors escaped "a zbad\\dir zdoes*exist" &&
395+
git -C escaped ls-tree -d --name-only HEAD | git -C escaped sparse-checkout set --stdin &&
396+
cat >expect <<-\EOF &&
397+
/*
398+
!/*/
399+
/deep/
400+
/folder1/
401+
/folder2/
402+
/zbad\\dir/
387403
/zdoes\*exist/
388404
EOF
389-
check_read_tree_errors escaped "a zbad\\dir zdoes*exist"
405+
test_cmp expect escaped/.git/info/sparse-checkout &&
406+
check_files escaped "a deep folder1 folder2 zbad\\dir zdoes*exist"
390407
'
391408

392409
test_done

0 commit comments

Comments
 (0)