Skip to content

Commit 24b9fd4

Browse files
jacob-kellerGit for Windows Build Agent
authored andcommitted
diff --no-index: fix logic for paths ending in '/'
If one of the two provided paths for git diff --no-index ends in a '/', a failure similar to the following occurs: $ git diff --no-index -- /tmp/ /tmp/ ':!' fatal: `pos + len' is too far after the end of the buffer This occurs because of an incorrect calculation of the skip lengths in diff_no_index(). The code wants to calculate the length of the string, but add one in case the string doesn't end with a slash. The method it uses is incorrect, as it always checks the trailing NUL character of the string. This will never be a '/', so we always add one. In the event that we *do* have a trailing slash, this will create an off-by-one length error later when using the skip value. The most straightforward fix would be to correct the skip1 and skip2 lengths by using ends_with(). However, Johannes made a good point that the existing logic is wasting a lot of computation. We generate the match string by copying the path in and then skipping almost all of it immediately with a potentially expensive memmove() from the strbuf_remove() call. We also re-initialize the match stringbuf each time we call read_directory_contents. The read_directory_contents really wants a path that is rooted at the start of the directory scan. We're currently building this by taking the full path and stripping out the start portion. Instead, replace this logic by building up the portion of the match as we go. Start by initializing two strbuf in diff_no_index containing the empty string. Pass these into queue_diff, which in turn passes the appropriate left or right side into read_directory_contents. As before, we build up the matches by appending elements to the match path and then clearing them using strbuf_setlen. In the recursive portion of the queue_diff algorithm, we build up new match paths the same way that we build up new buffer paths, by appending the elements and then clearing them with strbuf_setlen after each iteration. This is cheaper as it avoids repeated allocations, and is a bit simpler to track what is going on. Add a couple of test cases that pass in paths already ending in '/', to ensure the tests cover this regression. Reported-by: Johannes Schindelin <[email protected]> Closes: https://lore.kernel.org/git/[email protected]/ Signed-off-by: Jacob Keller <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
1 parent f8ca6c9 commit 24b9fd4

File tree

2 files changed

+49
-28
lines changed

2 files changed

+49
-28
lines changed

diff-no-index.c

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,21 @@
2121

2222
static int read_directory_contents(const char *path, struct string_list *list,
2323
const struct pathspec *pathspec,
24-
int skip)
24+
struct strbuf *match)
2525
{
26-
struct strbuf match = STRBUF_INIT;
27-
int len;
26+
int len = match->len;
2827
DIR *dir;
2928
struct dirent *e;
3029

3130
if (!(dir = opendir(path)))
3231
return error("Could not open directory %s", path);
3332

34-
if (pathspec) {
35-
strbuf_addstr(&match, path);
36-
strbuf_complete(&match, '/');
37-
strbuf_remove(&match, 0, skip);
38-
39-
len = match.len;
40-
}
41-
4233
while ((e = readdir_skip_dot_and_dotdot(dir))) {
4334
if (pathspec) {
4435
int is_dir = 0;
4536

46-
strbuf_setlen(&match, len);
47-
strbuf_addstr(&match, e->d_name);
37+
strbuf_setlen(match, len);
38+
strbuf_addstr(match, e->d_name);
4839
if (NOT_CONSTANT(DTYPE(e)) != DT_UNKNOWN) {
4940
is_dir = (DTYPE(e) == DT_DIR);
5041
} else {
@@ -57,15 +48,15 @@ static int read_directory_contents(const char *path, struct string_list *list,
5748
}
5849

5950
if (!match_leading_pathspec(NULL, pathspec,
60-
match.buf, match.len,
51+
match->buf, match->len,
6152
0, NULL, is_dir))
6253
continue;
6354
}
6455

6556
string_list_insert(list, e->d_name);
6657
}
6758

68-
strbuf_release(&match);
59+
strbuf_setlen(match, len);
6960
closedir(dir);
7061
return 0;
7162
}
@@ -169,7 +160,8 @@ static struct diff_filespec *noindex_filespec(const struct git_hash_algo *algop,
169160

170161
static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
171162
const char *name1, const char *name2, int recursing,
172-
const struct pathspec *ps, int skip1, int skip2)
163+
const struct pathspec *ps,
164+
struct strbuf *ps_match1, struct strbuf *ps_match2)
173165
{
174166
int mode1 = 0, mode2 = 0;
175167
enum special special1 = SPECIAL_NONE, special2 = SPECIAL_NONE;
@@ -208,10 +200,12 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
208200
struct string_list p2 = STRING_LIST_INIT_DUP;
209201
int i1, i2, ret = 0;
210202
size_t len1 = 0, len2 = 0;
203+
size_t match1_len = ps_match1->len;
204+
size_t match2_len = ps_match2->len;
211205

212-
if (name1 && read_directory_contents(name1, &p1, ps, skip1))
206+
if (name1 && read_directory_contents(name1, &p1, ps, ps_match1))
213207
return -1;
214-
if (name2 && read_directory_contents(name2, &p2, ps, skip2)) {
208+
if (name2 && read_directory_contents(name2, &p2, ps, ps_match2)) {
215209
string_list_clear(&p1, 0);
216210
return -1;
217211
}
@@ -235,6 +229,11 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
235229
strbuf_setlen(&buffer1, len1);
236230
strbuf_setlen(&buffer2, len2);
237231

232+
if (ps) {
233+
strbuf_setlen(ps_match1, match1_len);
234+
strbuf_setlen(ps_match2, match2_len);
235+
}
236+
238237
if (i1 == p1.nr)
239238
comp = 1;
240239
else if (i2 == p2.nr)
@@ -245,18 +244,28 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
245244
if (comp > 0)
246245
n1 = NULL;
247246
else {
248-
strbuf_addstr(&buffer1, p1.items[i1++].string);
247+
strbuf_addstr(&buffer1, p1.items[i1].string);
248+
if (ps) {
249+
strbuf_addstr(ps_match1, p1.items[i1].string);
250+
strbuf_complete(ps_match1, '/');
251+
}
249252
n1 = buffer1.buf;
253+
i1++;
250254
}
251255

252256
if (comp < 0)
253257
n2 = NULL;
254258
else {
255-
strbuf_addstr(&buffer2, p2.items[i2++].string);
259+
strbuf_addstr(&buffer2, p2.items[i2].string);
260+
if (ps) {
261+
strbuf_addstr(ps_match2, p2.items[i2].string);
262+
strbuf_complete(ps_match2, '/');
263+
}
256264
n2 = buffer2.buf;
265+
i2++;
257266
}
258267

259-
ret = queue_diff(o, algop, n1, n2, 1, ps, skip1, skip2);
268+
ret = queue_diff(o, algop, n1, n2, 1, ps, ps_match1, ps_match2);
260269
}
261270
string_list_clear(&p1, 0);
262271
string_list_clear(&p2, 0);
@@ -346,7 +355,8 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
346355
int implicit_no_index, int argc, const char **argv)
347356
{
348357
struct pathspec pathspec, *ps = NULL;
349-
int i, no_index, skip1 = 0, skip2 = 0;
358+
struct strbuf ps_match1 = STRBUF_INIT, ps_match2 = STRBUF_INIT;
359+
int i, no_index;
350360
int ret = 1;
351361
const char *paths[2];
352362
char *to_free[ARRAY_SIZE(paths)] = { 0 };
@@ -387,11 +397,6 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
387397
NULL, &argv[2]);
388398
if (pathspec.nr)
389399
ps = &pathspec;
390-
391-
skip1 = strlen(paths[0]);
392-
skip1 += paths[0][skip1] == '/' ? 0 : 1;
393-
skip2 = strlen(paths[1]);
394-
skip2 += paths[1][skip2] == '/' ? 0 : 1;
395400
} else if (argc > 2) {
396401
warning(_("Limiting comparison with pathspecs is only "
397402
"supported if both paths are directories."));
@@ -415,7 +420,7 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
415420
revs->diffopt.flags.exit_with_status = 1;
416421

417422
if (queue_diff(&revs->diffopt, algop, paths[0], paths[1], 0, ps,
418-
skip1, skip2))
423+
&ps_match1, &ps_match2))
419424
goto out;
420425
diff_set_mnemonic_prefix(&revs->diffopt, "1/", "2/");
421426
diffcore_std(&revs->diffopt);

t/t4053-diff-no-index.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,22 @@ test_expect_success 'diff --no-index with pathspec' '
339339
test_cmp expect actual
340340
'
341341

342+
test_expect_success 'diff --no-index first path ending in slash with pathspec' '
343+
test_expect_code 1 git diff --name-status --no-index a/ b 1 >actual &&
344+
cat >expect <<-EOF &&
345+
D a/1
346+
EOF
347+
test_cmp expect actual
348+
'
349+
350+
test_expect_success 'diff --no-index second path ending in slash with pathspec' '
351+
test_expect_code 1 git diff --name-status --no-index a b/ 1 >actual &&
352+
cat >expect <<-EOF &&
353+
D a/1
354+
EOF
355+
test_cmp expect actual
356+
'
357+
342358
test_expect_success 'diff --no-index with pathspec no matches' '
343359
test_expect_code 0 git diff --name-status --no-index a b missing
344360
'

0 commit comments

Comments
 (0)