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
This commit is contained in:
Junio C Hamano
2025-06-17 10:44:41 -07:00
7 changed files with 183 additions and 25 deletions

View File

@@ -14,7 +14,7 @@ git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]
git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]
git diff [<options>] <commit>...<commit> [--] [<path>...]
git diff [<options>] <blob> <blob>
git diff [<options>] --no-index [--] <path> <path>
git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]
DESCRIPTION
-----------
@@ -31,14 +31,18 @@ files on disk.
further add to the index but you still haven't. You can
stage these changes by using linkgit:git-add[1].
`git diff [<options>] --no-index [--] <path> <path>`::
`git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]`::
This form is to compare the given two paths on the
filesystem. You can omit the `--no-index` option when
running the command in a working tree controlled by Git and
at least one of the paths points outside the working tree,
or when running the command outside a working tree
controlled by Git. This form implies `--exit-code`.
controlled by Git. This form implies `--exit-code`. If both
paths point to directories, additional pathspecs may be
provided. These will limit the files included in the
difference. All such pathspecs must be relative as they
apply to both sides of the diff.
`git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]`::

View File

@@ -35,7 +35,7 @@ static const char builtin_diff_usage[] =
" or: git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]\n"
" or: git diff [<options>] <commit>...<commit> [--] [<path>...]\n"
" or: git diff [<options>] <blob> <blob>\n"
" or: git diff [<options>] --no-index [--] <path> <path>"
" or: git diff [<options>] --no-index [--] <path> <path> [<pathspec>...]"
"\n"
COMMON_DIFF_OPTIONS_HELP;

View File

@@ -15,20 +15,45 @@
#include "gettext.h"
#include "revision.h"
#include "parse-options.h"
#include "pathspec.h"
#include "string-list.h"
#include "dir.h"
static int read_directory_contents(const char *path, struct string_list *list)
static int read_directory_contents(const char *path, struct string_list *list,
const struct pathspec *pathspec,
int skip)
{
struct strbuf match = STRBUF_INIT;
int len;
DIR *dir;
struct dirent *e;
if (!(dir = opendir(path)))
return error("Could not open directory %s", path);
while ((e = readdir_skip_dot_and_dotdot(dir)))
string_list_insert(list, e->d_name);
if (pathspec) {
strbuf_addstr(&match, path);
strbuf_complete(&match, '/');
strbuf_remove(&match, 0, skip);
len = match.len;
}
while ((e = readdir_skip_dot_and_dotdot(dir))) {
if (pathspec) {
strbuf_setlen(&match, len);
strbuf_addstr(&match, e->d_name);
if (!match_leading_pathspec(NULL, pathspec,
match.buf, match.len,
0, NULL, e->d_type == DT_DIR ? 1 : 0))
continue;
}
string_list_insert(list, e->d_name);
}
strbuf_release(&match);
closedir(dir);
return 0;
}
@@ -131,7 +156,8 @@ static struct diff_filespec *noindex_filespec(const struct git_hash_algo *algop,
}
static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
const char *name1, const char *name2, int recursing)
const char *name1, const char *name2, int recursing,
const struct pathspec *ps, int skip1, int skip2)
{
int mode1 = 0, mode2 = 0;
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,
int i1, i2, ret = 0;
size_t len1 = 0, len2 = 0;
if (name1 && read_directory_contents(name1, &p1))
if (name1 && read_directory_contents(name1, &p1, ps, skip1))
return -1;
if (name2 && read_directory_contents(name2, &p2)) {
if (name2 && read_directory_contents(name2, &p2, ps, skip2)) {
string_list_clear(&p1, 0);
return -1;
}
@@ -218,7 +244,7 @@ static int queue_diff(struct diff_options *o, const struct git_hash_algo *algop,
n2 = buffer2.buf;
}
ret = queue_diff(o, algop, n1, n2, 1);
ret = queue_diff(o, algop, n1, n2, 1, ps, skip1, skip2);
}
string_list_clear(&p1, 0);
string_list_clear(&p2, 0);
@@ -258,8 +284,10 @@ static void append_basename(struct strbuf *path, const char *dir, const char *fi
* DWIM "diff D F" into "diff D/F F" and "diff F D" into "diff F D/F"
* Note that we append the basename of F to D/, so "diff a/b/file D"
* becomes "diff a/b/file D/file", not "diff a/b/file D/a/b/file".
*
* Return 1 if both paths are directories, 0 otherwise.
*/
static void fixup_paths(const char **path, struct strbuf *replacement)
static int fixup_paths(const char **path, struct strbuf *replacement)
{
struct stat st;
unsigned int isdir0 = 0, isdir1 = 0;
@@ -282,26 +310,31 @@ static void fixup_paths(const char **path, struct strbuf *replacement)
if ((isdir0 && ispipe1) || (ispipe0 && isdir1))
die(_("cannot compare a named pipe to a directory"));
if (isdir0 == isdir1)
return;
/* if both paths are directories, we will enable pathspecs */
if (isdir0 && isdir1)
return 1;
if (isdir0) {
append_basename(replacement, path[0], path[1]);
path[0] = replacement->buf;
} else {
} else if (isdir1) {
append_basename(replacement, path[1], path[0]);
path[1] = replacement->buf;
}
return 0;
}
static const char * const diff_no_index_usage[] = {
N_("git diff --no-index [<options>] <path> <path>"),
N_("git diff --no-index [<options>] <path> <path> [<pathspec>...]"),
NULL
};
int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
int implicit_no_index, int argc, const char **argv)
{
int i, no_index;
struct pathspec pathspec, *ps = NULL;
int i, no_index, skip1 = 0, skip2 = 0;
int ret = 1;
const char *paths[2];
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,
options = add_diff_options(no_index_options, &revs->diffopt);
argc = parse_options(argc, argv, revs->prefix, options,
diff_no_index_usage, 0);
if (argc != 2) {
if (argc < 2) {
if (implicit_no_index)
warning(_("Not a git repository. Use --no-index to "
"compare two paths outside a working tree"));
usage_with_options(diff_no_index_usage, options);
}
FREE_AND_NULL(options);
for (i = 0; i < 2; i++) {
const char *p = argv[i];
if (!strcmp(p, "-"))
@@ -337,7 +369,23 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
paths[i] = p;
}
fixup_paths(paths, &replacement);
if (fixup_paths(paths, &replacement)) {
parse_pathspec(&pathspec, PATHSPEC_FROMTOP | PATHSPEC_ATTR,
PATHSPEC_PREFER_FULL | PATHSPEC_NO_REPOSITORY,
NULL, &argv[2]);
if (pathspec.nr)
ps = &pathspec;
skip1 = strlen(paths[0]);
skip1 += paths[0][skip1] == '/' ? 0 : 1;
skip2 = strlen(paths[1]);
skip2 += paths[1][skip2] == '/' ? 0 : 1;
} else if (argc > 2) {
warning(_("Limiting comparison with pathspecs is only "
"supported if both paths are directories."));
usage_with_options(diff_no_index_usage, options);
}
FREE_AND_NULL(options);
revs->diffopt.skip_stat_unmatch = 1;
if (!revs->diffopt.output_format)
@@ -354,7 +402,8 @@ int diff_no_index(struct rev_info *revs, const struct git_hash_algo *algop,
setup_diff_pager(&revs->diffopt);
revs->diffopt.flags.exit_with_status = 1;
if (queue_diff(&revs->diffopt, algop, paths[0], paths[1], 0))
if (queue_diff(&revs->diffopt, algop, paths[0], paths[1], 0, ps,
skip1, skip2))
goto out;
diff_set_mnemonic_prefix(&revs->diffopt, "1/", "2/");
diffcore_std(&revs->diffopt);
@@ -370,5 +419,7 @@ out:
for (i = 0; i < ARRAY_SIZE(to_free); i++)
free(to_free[i]);
strbuf_release(&replacement);
if (ps)
clear_pathspec(ps);
return ret;
}

19
dir.c
View File

@@ -397,9 +397,12 @@ static int match_pathspec_item(struct index_state *istate,
strncmp(item->match, name - prefix, item->prefix))
return 0;
if (item->attr_match_nr &&
!match_pathspec_attrs(istate, name - prefix, namelen + prefix, item))
return 0;
if (item->attr_match_nr) {
if (!istate)
BUG("magic PATHSPEC_ATTR requires an index");
if (!match_pathspec_attrs(istate, name - prefix, namelen + prefix, item))
return 0;
}
/* If the match was just the prefix, we matched */
if (!*match)
@@ -577,6 +580,16 @@ int match_pathspec(struct index_state *istate,
prefix, seen, flags);
}
int match_leading_pathspec(struct index_state *istate,
const struct pathspec *ps,
const char *name, int namelen,
int prefix, char *seen, int is_dir)
{
unsigned flags = is_dir ? DO_MATCH_DIRECTORY | DO_MATCH_LEADING_PATHSPEC : 0;
return match_pathspec_with_flags(istate, ps, name, namelen,
prefix, seen, flags);
}
/**
* Check if a submodule is a superset of the pathspec
*/

View File

@@ -492,7 +492,7 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
if (!match) {
const char *hint_path;
if (!have_git_dir())
if ((flags & PATHSPEC_NO_REPOSITORY) || !have_git_dir())
die(_("'%s' is outside the directory tree"),
copyfrom);
hint_path = repo_get_work_tree(the_repository);
@@ -614,6 +614,10 @@ void parse_pathspec(struct pathspec *pathspec,
(flags & PATHSPEC_PREFER_FULL))
BUG("PATHSPEC_PREFER_CWD and PATHSPEC_PREFER_FULL are incompatible");
if ((flags & PATHSPEC_NO_REPOSITORY) &&
(~magic_mask & (PATHSPEC_ATTR | PATHSPEC_FROMTOP)))
BUG("PATHSPEC_NO_REPOSITORY is incompatible with PATHSPEC_ATTR and PATHSPEC_FROMTOP");
/* No arguments with prefix -> prefix pathspec */
if (!entry) {
if (flags & PATHSPEC_PREFER_FULL)

View File

@@ -76,6 +76,11 @@ struct pathspec {
* allowed, then it will automatically set for every pathspec.
*/
#define PATHSPEC_LITERAL_PATH (1<<6)
/*
* For git diff --no-index, indicate that we are operating without
* a repository or index.
*/
#define PATHSPEC_NO_REPOSITORY (1<<7)
/**
* Given command line arguments and a prefix, convert the input to
@@ -184,6 +189,12 @@ int match_pathspec(struct index_state *istate,
const char *name, int namelen,
int prefix, char *seen, int is_dir);
/* Set both DO_MATCH_DIRECTORY and DO_MATCH_LEADING_PATHSPEC if is_dir true */
int match_leading_pathspec(struct index_state *istate,
const struct pathspec *ps,
const char *name, int namelen,
int prefix, char *seen, int is_dir);
/*
* Determine whether a pathspec will match only entire index entries (non-sparse
* files and/or entire sparse directories). If the pathspec has the potential to

View File

@@ -295,4 +295,79 @@ test_expect_success PIPE,SYMLINKS 'diff --no-index reads from pipes' '
test_cmp expect actual
'
test_expect_success 'diff --no-index F F rejects pathspecs' '
test_must_fail git diff --no-index -- a/1 a/2 a 2>actual.err &&
test_grep "usage: git diff --no-index" actual.err
'
test_expect_success 'diff --no-index D F rejects pathspecs' '
test_must_fail git diff --no-index -- a a/2 a 2>actual.err &&
test_grep "usage: git diff --no-index" actual.err
'
test_expect_success 'diff --no-index F D rejects pathspecs' '
test_must_fail git diff --no-index -- a/1 b b 2>actual.err &&
test_grep "usage: git diff --no-index" actual.err
'
test_expect_success 'diff --no-index rejects absolute pathspec' '
test_must_fail git diff --no-index -- a b $(pwd)/a/1
'
test_expect_success 'diff --no-index with pathspec' '
test_expect_code 1 git diff --name-status --no-index a b 1 >actual &&
cat >expect <<-EOF &&
D a/1
EOF
test_cmp expect actual
'
test_expect_success 'diff --no-index with pathspec no matches' '
test_expect_code 0 git diff --name-status --no-index a b missing
'
test_expect_success 'diff --no-index with negative pathspec' '
test_expect_code 1 git diff --name-status --no-index a b ":!2" >actual &&
cat >expect <<-EOF &&
D a/1
EOF
test_cmp expect actual
'
test_expect_success 'setup nested' '
mkdir -p c/1/2 &&
mkdir -p d/1/2 &&
echo 1 >c/1/2/a &&
echo 2 >c/1/2/b
'
test_expect_success 'diff --no-index with pathspec nested negative pathspec' '
test_expect_code 0 git diff --no-index c d ":!1"
'
test_expect_success 'diff --no-index with pathspec nested pathspec' '
test_expect_code 1 git diff --name-status --no-index c d 1/2 >actual &&
cat >expect <<-EOF &&
D c/1/2/a
D c/1/2/b
EOF
test_cmp expect actual
'
test_expect_success 'diff --no-index with pathspec glob' '
test_expect_code 1 git diff --name-status --no-index c d ":(glob)**/a" >actual &&
cat >expect <<-EOF &&
D c/1/2/a
EOF
test_cmp expect actual
'
test_expect_success 'diff --no-index with pathspec glob and exclude' '
test_expect_code 1 git diff --name-status --no-index c d ":(glob,exclude)**/a" >actual &&
cat >expect <<-EOF &&
D c/1/2/b
EOF
test_cmp expect actual
'
test_done