Skip to content

Commit 2024ab3

Browse files
committed
Merge branch 'jk/diff-no-index-with-pathspec'
"git diff --no-index dirA dirB" can limit the comparison with pathspec at the end of the command line, just like normal "git diff". * jk/diff-no-index-with-pathspec: diff --no-index: support limiting by pathspec pathspec: add flag to indicate operation without repository pathspec: add match_leading_pathspec variant
2 parents 5e22d03 + 09fb155 commit 2024ab3

File tree

7 files changed

+182
-24
lines changed

7 files changed

+182
-24
lines changed

Documentation/git-diff.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
1414
git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]
1515
git diff [<options>] <commit>...<commit> [--] [<path>...]
1616
git diff [<options>] <blob> <blob>
17-
git diff [<options>] --no-index [--] <path> <path>
17+
git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]
1818

1919
DESCRIPTION
2020
-----------
@@ -31,14 +31,18 @@ files on disk.
3131
further add to the index but you still haven't. You can
3232
stage these changes by using linkgit:git-add[1].
3333

34-
`git diff [<options>] --no-index [--] <path> <path>`::
34+
`git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]`::
3535

3636
This form is to compare the given two paths on the
3737
filesystem. You can omit the `--no-index` option when
3838
running the command in a working tree controlled by Git and
3939
at least one of the paths points outside the working tree,
4040
or when running the command outside a working tree
41-
controlled by Git. This form implies `--exit-code`.
41+
controlled by Git. This form implies `--exit-code`. If both
42+
paths point to directories, additional pathspecs may be
43+
provided. These will limit the files included in the
44+
difference. All such pathspecs must be relative as they
45+
apply to both sides of the diff.
4246

4347
`git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]`::
4448

builtin/diff.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static const char builtin_diff_usage[] =
3535
" or: git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]\n"
3636
" or: git diff [<options>] <commit>...<commit> [--] [<path>...]\n"
3737
" or: git diff [<options>] <blob> <blob>\n"
38-
" or: git diff [<options>] --no-index [--] <path> <path>"
38+
" or: git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]"
3939
"\n"
4040
COMMON_DIFF_OPTIONS_HELP;
4141

diff-no-index.c

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,45 @@
1515
#include "gettext.h"
1616
#include "revision.h"
1717
#include "parse-options.h"
18+
#include "pathspec.h"
1819
#include "string-list.h"
1920
#include "dir.h"
2021

21-
static int read_directory_contents(const char *path, struct string_list *list)
22+
static int read_directory_contents(const char *path, struct string_list *list,
23+
const struct pathspec *pathspec,
24+
int skip)
2225
{
26+
struct strbuf match = STRBUF_INIT;
27+
int len;
2328
DIR *dir;
2429
struct dirent *e;
2530

2631
if (!(dir = opendir(path)))
2732
return error("Could not open directory %s", path);
2833

29-
while ((e = readdir_skip_dot_and_dotdot(dir)))
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+
42+
while ((e = readdir_skip_dot_and_dotdot(dir))) {
43+
if (pathspec) {
44+
strbuf_setlen(&match, len);
45+
strbuf_addstr(&match, e->d_name);
46+
47+
if (!match_leading_pathspec(NULL, pathspec,
48+
match.buf, match.len,
49+
0, NULL, e->d_type == DT_DIR ? 1 : 0))
50+
continue;
51+
}
52+
3053
string_list_insert(list, e->d_name);
54+
}
3155

56+
strbuf_release(&match);
3257
closedir(dir);
3358
return 0;
3459
}
@@ -131,7 +156,8 @@ static struct diff_filespec *noindex_filespec(const struct git_hash_algo *algop,
131156
}
132157

133158
static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
134-
const char *name1, const char *name2, int recursing)
159+
const char *name1, const char *name2, int recursing,
160+
const struct pathspec *ps, int skip1, int skip2)
135161
{
136162
int mode1 = 0, mode2 = 0;
137163
enum special special1 = SPECIAL_NONE, special2 = SPECIAL_NONE;
@@ -171,9 +197,9 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
171197
int i1, i2, ret = 0;
172198
size_t len1 = 0, len2 = 0;
173199

174-
if (name1 && read_directory_contents(name1, &p1))
200+
if (name1 && read_directory_contents(name1, &p1, ps, skip1))
175201
return -1;
176-
if (name2 && read_directory_contents(name2, &p2)) {
202+
if (name2 && read_directory_contents(name2, &p2, ps, skip2)) {
177203
string_list_clear(&p1, 0);
178204
return -1;
179205
}
@@ -218,7 +244,7 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
218244
n2 = buffer2.buf;
219245
}
220246

221-
ret = queue_diff(o, algop, n1, n2, 1);
247+
ret = queue_diff(o, algop, n1, n2, 1, ps, skip1, skip2);
222248
}
223249
string_list_clear(&p1, 0);
224250
string_list_clear(&p2, 0);
@@ -258,8 +284,10 @@ static void append_basename(struct strbuf *path, const char *dir, const char *fi
258284
* DWIM "diff D F" into "diff D/F F" and "diff F D" into "diff F D/F"
259285
* Note that we append the basename of F to D/, so "diff a/b/file D"
260286
* becomes "diff a/b/file D/file", not "diff a/b/file D/a/b/file".
287+
*
288+
* Return 1 if both paths are directories, 0 otherwise.
261289
*/
262-
static void fixup_paths(const char **path, struct strbuf *replacement)
290+
static int fixup_paths(const char **path, struct strbuf *replacement)
263291
{
264292
struct stat st;
265293
unsigned int isdir0 = 0, isdir1 = 0;
@@ -282,26 +310,31 @@ static void fixup_paths(const char **path, struct strbuf *replacement)
282310
if ((isdir0 && ispipe1) || (ispipe0 && isdir1))
283311
die(_("cannot compare a named pipe to a directory"));
284312

285-
if (isdir0 == isdir1)
286-
return;
313+
/* if both paths are directories, we will enable pathspecs */
314+
if (isdir0 && isdir1)
315+
return 1;
316+
287317
if (isdir0) {
288318
append_basename(replacement, path[0], path[1]);
289319
path[0] = replacement->buf;
290-
} else {
320+
} else if (isdir1) {
291321
append_basename(replacement, path[1], path[0]);
292322
path[1] = replacement->buf;
293323
}
324+
325+
return 0;
294326
}
295327

296328
static const char * const diff_no_index_usage[] = {
297-
N_("git diff --no-index [<options>] <path> <path>"),
329+
N_("git diff --no-index [<options>] <path> <path> [<pathspec>...]"),
298330
NULL
299331
};
300332

301333
int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
302334
int implicit_no_index, int argc, const char **argv)
303335
{
304-
int i, no_index;
336+
struct pathspec pathspec, *ps = NULL;
337+
int i, no_index, skip1 = 0, skip2 = 0;
305338
int ret = 1;
306339
const char *paths[2];
307340
char *to_free[ARRAY_SIZE(paths)] = { 0 };
@@ -317,13 +350,12 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
317350
options = add_diff_options(no_index_options, &revs->diffopt);
318351
argc = parse_options(argc, argv, revs->prefix, options,
319352
diff_no_index_usage, 0);
320-
if (argc != 2) {
353+
if (argc < 2) {
321354
if (implicit_no_index)
322355
warning(_("Not a git repository. Use --no-index to "
323356
"compare two paths outside a working tree"));
324357
usage_with_options(diff_no_index_usage, options);
325358
}
326-
FREE_AND_NULL(options);
327359
for (i = 0; i < 2; i++) {
328360
const char *p = argv[i];
329361
if (!strcmp(p, "-"))
@@ -337,7 +369,23 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
337369
paths[i] = p;
338370
}
339371

340-
fixup_paths(paths, &replacement);
372+
if (fixup_paths(paths, &replacement)) {
373+
parse_pathspec(&pathspec, PATHSPEC_FROMTOP | PATHSPEC_ATTR,
374+
PATHSPEC_PREFER_FULL | PATHSPEC_NO_REPOSITORY,
375+
NULL, &argv[2]);
376+
if (pathspec.nr)
377+
ps = &pathspec;
378+
379+
skip1 = strlen(paths[0]);
380+
skip1 += paths[0][skip1] == '/' ? 0 : 1;
381+
skip2 = strlen(paths[1]);
382+
skip2 += paths[1][skip2] == '/' ? 0 : 1;
383+
} else if (argc > 2) {
384+
warning(_("Limiting comparison with pathspecs is only "
385+
"supported if both paths are directories."));
386+
usage_with_options(diff_no_index_usage, options);
387+
}
388+
FREE_AND_NULL(options);
341389

342390
revs->diffopt.skip_stat_unmatch = 1;
343391
if (!revs->diffopt.output_format)
@@ -354,7 +402,8 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
354402
setup_diff_pager(&revs->diffopt);
355403
revs->diffopt.flags.exit_with_status = 1;
356404

357-
if (queue_diff(&revs->diffopt, algop, paths[0], paths[1], 0))
405+
if (queue_diff(&revs->diffopt, algop, paths[0], paths[1], 0, ps,
406+
skip1, skip2))
358407
goto out;
359408
diff_set_mnemonic_prefix(&revs->diffopt, "1/", "2/");
360409
diffcore_std(&revs->diffopt);
@@ -370,5 +419,7 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
370419
for (i = 0; i < ARRAY_SIZE(to_free); i++)
371420
free(to_free[i]);
372421
strbuf_release(&replacement);
422+
if (ps)
423+
clear_pathspec(ps);
373424
return ret;
374425
}

dir.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,12 @@ static int match_pathspec_item(struct index_state *istate,
397397
strncmp(item->match, name - prefix, item->prefix))
398398
return 0;
399399

400-
if (item->attr_match_nr &&
401-
!match_pathspec_attrs(istate, name - prefix, namelen + prefix, item))
402-
return 0;
400+
if (item->attr_match_nr) {
401+
if (!istate)
402+
BUG("magic PATHSPEC_ATTR requires an index");
403+
if (!match_pathspec_attrs(istate, name - prefix, namelen + prefix, item))
404+
return 0;
405+
}
403406

404407
/* If the match was just the prefix, we matched */
405408
if (!*match)
@@ -577,6 +580,16 @@ int match_pathspec(struct index_state *istate,
577580
prefix, seen, flags);
578581
}
579582

583+
int match_leading_pathspec(struct index_state *istate,
584+
const struct pathspec *ps,
585+
const char *name, int namelen,
586+
int prefix, char *seen, int is_dir)
587+
{
588+
unsigned flags = is_dir ? DO_MATCH_DIRECTORY | DO_MATCH_LEADING_PATHSPEC : 0;
589+
return match_pathspec_with_flags(istate, ps, name, namelen,
590+
prefix, seen, flags);
591+
}
592+
580593
/**
581594
* Check if a submodule is a superset of the pathspec
582595
*/

pathspec.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
492492
if (!match) {
493493
const char *hint_path;
494494

495-
if (!have_git_dir())
495+
if ((flags & PATHSPEC_NO_REPOSITORY) || !have_git_dir())
496496
die(_("'%s' is outside the directory tree"),
497497
copyfrom);
498498
hint_path = repo_get_work_tree(the_repository);
@@ -614,6 +614,10 @@ void parse_pathspec(struct pathspec *pathspec,
614614
(flags & PATHSPEC_PREFER_FULL))
615615
BUG("PATHSPEC_PREFER_CWD and PATHSPEC_PREFER_FULL are incompatible");
616616

617+
if ((flags & PATHSPEC_NO_REPOSITORY) &&
618+
(~magic_mask & (PATHSPEC_ATTR | PATHSPEC_FROMTOP)))
619+
BUG("PATHSPEC_NO_REPOSITORY is incompatible with PATHSPEC_ATTR and PATHSPEC_FROMTOP");
620+
617621
/* No arguments with prefix -> prefix pathspec */
618622
if (!entry) {
619623
if (flags & PATHSPEC_PREFER_FULL)

pathspec.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ struct pathspec {
7676
* allowed, then it will automatically set for every pathspec.
7777
*/
7878
#define PATHSPEC_LITERAL_PATH (1<<6)
79+
/*
80+
* For git diff --no-index, indicate that we are operating without
81+
* a repository or index.
82+
*/
83+
#define PATHSPEC_NO_REPOSITORY (1<<7)
7984

8085
/**
8186
* Given command line arguments and a prefix, convert the input to
@@ -184,6 +189,12 @@ int match_pathspec(struct index_state *istate,
184189
const char *name, int namelen,
185190
int prefix, char *seen, int is_dir);
186191

192+
/* Set both DO_MATCH_DIRECTORY and DO_MATCH_LEADING_PATHSPEC if is_dir true */
193+
int match_leading_pathspec(struct index_state *istate,
194+
const struct pathspec *ps,
195+
const char *name, int namelen,
196+
int prefix, char *seen, int is_dir);
197+
187198
/*
188199
* Determine whether a pathspec will match only entire index entries (non-sparse
189200
* files and/or entire sparse directories). If the pathspec has the potential to

t/t4053-diff-no-index.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,79 @@ test_expect_success PIPE,SYMLINKS 'diff --no-index reads from pipes' '
295295
test_cmp expect actual
296296
'
297297

298+
test_expect_success 'diff --no-index F F rejects pathspecs' '
299+
test_must_fail git diff --no-index -- a/1 a/2 a 2>actual.err &&
300+
test_grep "usage: git diff --no-index" actual.err
301+
'
302+
303+
test_expect_success 'diff --no-index D F rejects pathspecs' '
304+
test_must_fail git diff --no-index -- a a/2 a 2>actual.err &&
305+
test_grep "usage: git diff --no-index" actual.err
306+
'
307+
308+
test_expect_success 'diff --no-index F D rejects pathspecs' '
309+
test_must_fail git diff --no-index -- a/1 b b 2>actual.err &&
310+
test_grep "usage: git diff --no-index" actual.err
311+
'
312+
313+
test_expect_success 'diff --no-index rejects absolute pathspec' '
314+
test_must_fail git diff --no-index -- a b $(pwd)/a/1
315+
'
316+
317+
test_expect_success 'diff --no-index with pathspec' '
318+
test_expect_code 1 git diff --name-status --no-index a b 1 >actual &&
319+
cat >expect <<-EOF &&
320+
D a/1
321+
EOF
322+
test_cmp expect actual
323+
'
324+
325+
test_expect_success 'diff --no-index with pathspec no matches' '
326+
test_expect_code 0 git diff --name-status --no-index a b missing
327+
'
328+
329+
test_expect_success 'diff --no-index with negative pathspec' '
330+
test_expect_code 1 git diff --name-status --no-index a b ":!2" >actual &&
331+
cat >expect <<-EOF &&
332+
D a/1
333+
EOF
334+
test_cmp expect actual
335+
'
336+
337+
test_expect_success 'setup nested' '
338+
mkdir -p c/1/2 &&
339+
mkdir -p d/1/2 &&
340+
echo 1 >c/1/2/a &&
341+
echo 2 >c/1/2/b
342+
'
343+
344+
test_expect_success 'diff --no-index with pathspec nested negative pathspec' '
345+
test_expect_code 0 git diff --no-index c d ":!1"
346+
'
347+
348+
test_expect_success 'diff --no-index with pathspec nested pathspec' '
349+
test_expect_code 1 git diff --name-status --no-index c d 1/2 >actual &&
350+
cat >expect <<-EOF &&
351+
D c/1/2/a
352+
D c/1/2/b
353+
EOF
354+
test_cmp expect actual
355+
'
356+
357+
test_expect_success 'diff --no-index with pathspec glob' '
358+
test_expect_code 1 git diff --name-status --no-index c d ":(glob)**/a" >actual &&
359+
cat >expect <<-EOF &&
360+
D c/1/2/a
361+
EOF
362+
test_cmp expect actual
363+
'
364+
365+
test_expect_success 'diff --no-index with pathspec glob and exclude' '
366+
test_expect_code 1 git diff --name-status --no-index c d ":(glob,exclude)**/a" >actual &&
367+
cat >expect <<-EOF &&
368+
D c/1/2/b
369+
EOF
370+
test_cmp expect actual
371+
'
372+
298373
test_done

0 commit comments

Comments
 (0)