Merge branch 'nd/untracked-cache'

Teach the index to optionally remember already seen untracked files
to speed up "git status" in a working tree with tons of cruft.

* nd/untracked-cache: (24 commits)
  git-status.txt: advertisement for untracked cache
  untracked cache: guard and disable on system changes
  mingw32: add uname()
  t7063: tests for untracked cache
  update-index: test the system before enabling untracked cache
  update-index: manually enable or disable untracked cache
  status: enable untracked cache
  untracked-cache: temporarily disable with $GIT_DISABLE_UNTRACKED_CACHE
  untracked cache: mark index dirty if untracked cache is updated
  untracked cache: print stats with $GIT_TRACE_UNTRACKED_STATS
  untracked cache: avoid racy timestamps
  read-cache.c: split racy stat test to a separate function
  untracked cache: invalidate at index addition or removal
  untracked cache: load from UNTR index extension
  untracked cache: save to an index extension
  ewah: add convenient wrapper ewah_serialize_strbuf()
  untracked cache: don't open non-existent .gitignore
  untracked cache: mark what dirs should be recursed/saved
  untracked cache: record/validate dir mtime and reuse cached output
  untracked cache: make a wrapper around {open,read,close}dir()
  ...
This commit is contained in:
Junio C Hamano 2015-05-26 13:24:45 -07:00
commit 38ccaf93bb
21 changed files with 1823 additions and 59 deletions

1
.gitignore vendored
View File

@ -184,6 +184,7 @@
/test-delta /test-delta
/test-dump-cache-tree /test-dump-cache-tree
/test-dump-split-index /test-dump-split-index
/test-dump-untracked-cache
/test-scrap-cache-tree /test-scrap-cache-tree
/test-genrandom /test-genrandom
/test-hashmap /test-hashmap

View File

@ -66,7 +66,10 @@ When `-u` option is not used, untracked files and directories are
shown (i.e. the same as specifying `normal`), to help you avoid shown (i.e. the same as specifying `normal`), to help you avoid
forgetting to add newly created files. Because it takes extra work forgetting to add newly created files. Because it takes extra work
to find untracked files in the filesystem, this mode may take some to find untracked files in the filesystem, this mode may take some
time in a large working tree. You can use `no` to have `git status` time in a large working tree.
Consider enabling untracked cache and split index if supported (see
`git update-index --untracked-cache` and `git update-index
--split-index`), Otherwise you can use `no` to have `git status`
return more quickly without showing untracked files. return more quickly without showing untracked files.
+ +
The default can be changed using the status.showUntrackedFiles The default can be changed using the status.showUntrackedFiles

View File

@ -170,6 +170,20 @@ may not support it yet.
the shared index file. This mode is designed for very large the shared index file. This mode is designed for very large
indexes that take a significant amount of time to read or write. indexes that take a significant amount of time to read or write.
--untracked-cache::
--no-untracked-cache::
Enable or disable untracked cache extension. This could speed
up for commands that involve determining untracked files such
as `git status`. The underlying operating system and file
system must change `st_mtime` field of a directory if files
are added or deleted in that directory.
--force-untracked-cache::
For safety, `--untracked-cache` performs tests on the working
directory to make sure untracked cache can be used. These
tests can take a few seconds. `--force-untracked-cache` can be
used to skip the tests.
\--:: \--::
Do not interpret any more arguments as options. Do not interpret any more arguments as options.

View File

@ -233,3 +233,65 @@ Git index format
The remaining index entries after replaced ones will be added to the The remaining index entries after replaced ones will be added to the
final index. These added entries are also sorted by entry name then final index. These added entries are also sorted by entry name then
stage. stage.
== Untracked cache
Untracked cache saves the untracked file list and necessary data to
verify the cache. The signature for this extension is { 'U', 'N',
'T', 'R' }.
The extension starts with
- A sequence of NUL-terminated strings, preceded by the size of the
sequence in variable width encoding. Each string describes the
environment where the cache can be used.
- Stat data of $GIT_DIR/info/exclude. See "Index entry" section from
ctime field until "file size".
- Stat data of core.excludesfile
- 32-bit dir_flags (see struct dir_struct)
- 160-bit SHA-1 of $GIT_DIR/info/exclude. Null SHA-1 means the file
does not exist.
- 160-bit SHA-1 of core.excludesfile. Null SHA-1 means the file does
not exist.
- NUL-terminated string of per-dir exclude file name. This usually
is ".gitignore".
- The number of following directory blocks, variable width
encoding. If this number is zero, the extension ends here with a
following NUL.
- A number of directory blocks in depth-first-search order, each
consists of
- The number of untracked entries, variable width encoding.
- The number of sub-directory blocks, variable width encoding.
- The directory name terminated by NUL.
- A number of untrached file/dir names terminated by NUL.
The remaining data of each directory block is grouped by type:
- An ewah bitmap, the n-th bit marks whether the n-th directory has
valid untracked cache entries.
- An ewah bitmap, the n-th bit records "check-only" bit of
read_directory_recursive() for the n-th directory.
- An ewah bitmap, the n-th bit indicates whether SHA-1 and stat data
is valid for the n-th directory and exists in the next data.
- An array of stat data. The n-th data corresponds with the n-th
"one" bit in the previous ewah bitmap.
- An array of SHA-1. The n-th SHA-1 corresponds with the n-th "one" bit
in the previous ewah bitmap.
- One NUL.

View File

@ -574,6 +574,7 @@ TEST_PROGRAMS_NEED_X += test-date
TEST_PROGRAMS_NEED_X += test-delta TEST_PROGRAMS_NEED_X += test-delta
TEST_PROGRAMS_NEED_X += test-dump-cache-tree TEST_PROGRAMS_NEED_X += test-dump-cache-tree
TEST_PROGRAMS_NEED_X += test-dump-split-index TEST_PROGRAMS_NEED_X += test-dump-split-index
TEST_PROGRAMS_NEED_X += test-dump-untracked-cache
TEST_PROGRAMS_NEED_X += test-genrandom TEST_PROGRAMS_NEED_X += test-genrandom
TEST_PROGRAMS_NEED_X += test-hashmap TEST_PROGRAMS_NEED_X += test-hashmap
TEST_PROGRAMS_NEED_X += test-index-version TEST_PROGRAMS_NEED_X += test-index-version

View File

@ -1366,13 +1366,14 @@ int cmd_status(int argc, const char **argv, const char *prefix)
refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL); refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL);
fd = hold_locked_index(&index_lock, 0); fd = hold_locked_index(&index_lock, 0);
if (0 <= fd)
update_index_if_able(&the_index, &index_lock);
s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0; s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0;
s.ignore_submodule_arg = ignore_submodule_arg; s.ignore_submodule_arg = ignore_submodule_arg;
wt_status_collect(&s); wt_status_collect(&s);
if (0 <= fd)
update_index_if_able(&the_index, &index_lock);
if (s.relative_paths) if (s.relative_paths)
s.prefix = prefix; s.prefix = prefix;

View File

@ -33,6 +33,7 @@ static int mark_valid_only;
static int mark_skip_worktree_only; static int mark_skip_worktree_only;
#define MARK_FLAG 1 #define MARK_FLAG 1
#define UNMARK_FLAG 2 #define UNMARK_FLAG 2
static struct strbuf mtime_dir = STRBUF_INIT;
__attribute__((format (printf, 1, 2))) __attribute__((format (printf, 1, 2)))
static void report(const char *fmt, ...) static void report(const char *fmt, ...)
@ -48,6 +49,166 @@ static void report(const char *fmt, ...)
va_end(vp); va_end(vp);
} }
static void remove_test_directory(void)
{
if (mtime_dir.len)
remove_dir_recursively(&mtime_dir, 0);
}
static const char *get_mtime_path(const char *path)
{
static struct strbuf sb = STRBUF_INIT;
strbuf_reset(&sb);
strbuf_addf(&sb, "%s/%s", mtime_dir.buf, path);
return sb.buf;
}
static void xmkdir(const char *path)
{
path = get_mtime_path(path);
if (mkdir(path, 0700))
die_errno(_("failed to create directory %s"), path);
}
static int xstat_mtime_dir(struct stat *st)
{
if (stat(mtime_dir.buf, st))
die_errno(_("failed to stat %s"), mtime_dir.buf);
return 0;
}
static int create_file(const char *path)
{
int fd;
path = get_mtime_path(path);
fd = open(path, O_CREAT | O_RDWR, 0644);
if (fd < 0)
die_errno(_("failed to create file %s"), path);
return fd;
}
static void xunlink(const char *path)
{
path = get_mtime_path(path);
if (unlink(path))
die_errno(_("failed to delete file %s"), path);
}
static void xrmdir(const char *path)
{
path = get_mtime_path(path);
if (rmdir(path))
die_errno(_("failed to delete directory %s"), path);
}
static void avoid_racy(void)
{
/*
* not use if we could usleep(10) if USE_NSEC is defined. The
* field nsec could be there, but the OS could choose to
* ignore it?
*/
sleep(1);
}
static int test_if_untracked_cache_is_supported(void)
{
struct stat st;
struct stat_data base;
int fd, ret = 0;
strbuf_addstr(&mtime_dir, "mtime-test-XXXXXX");
if (!mkdtemp(mtime_dir.buf))
die_errno("Could not make temporary directory");
fprintf(stderr, _("Testing "));
atexit(remove_test_directory);
xstat_mtime_dir(&st);
fill_stat_data(&base, &st);
fputc('.', stderr);
avoid_racy();
fd = create_file("newfile");
xstat_mtime_dir(&st);
if (!match_stat_data(&base, &st)) {
close(fd);
fputc('\n', stderr);
fprintf_ln(stderr,_("directory stat info does not "
"change after adding a new file"));
goto done;
}
fill_stat_data(&base, &st);
fputc('.', stderr);
avoid_racy();
xmkdir("new-dir");
xstat_mtime_dir(&st);
if (!match_stat_data(&base, &st)) {
close(fd);
fputc('\n', stderr);
fprintf_ln(stderr, _("directory stat info does not change "
"after adding a new directory"));
goto done;
}
fill_stat_data(&base, &st);
fputc('.', stderr);
avoid_racy();
write_or_die(fd, "data", 4);
close(fd);
xstat_mtime_dir(&st);
if (match_stat_data(&base, &st)) {
fputc('\n', stderr);
fprintf_ln(stderr, _("directory stat info changes "
"after updating a file"));
goto done;
}
fputc('.', stderr);
avoid_racy();
close(create_file("new-dir/new"));
xstat_mtime_dir(&st);
if (match_stat_data(&base, &st)) {
fputc('\n', stderr);
fprintf_ln(stderr, _("directory stat info changes after "
"adding a file inside subdirectory"));
goto done;
}
fputc('.', stderr);
avoid_racy();
xunlink("newfile");
xstat_mtime_dir(&st);
if (!match_stat_data(&base, &st)) {
fputc('\n', stderr);
fprintf_ln(stderr, _("directory stat info does not "
"change after deleting a file"));
goto done;
}
fill_stat_data(&base, &st);
fputc('.', stderr);
avoid_racy();
xunlink("new-dir/new");
xrmdir("new-dir");
xstat_mtime_dir(&st);
if (!match_stat_data(&base, &st)) {
fputc('\n', stderr);
fprintf_ln(stderr, _("directory stat info does not "
"change after deleting a directory"));
goto done;
}
if (rmdir(mtime_dir.buf))
die_errno(_("failed to delete directory %s"), mtime_dir.buf);
fprintf_ln(stderr, _(" OK"));
ret = 1;
done:
strbuf_release(&mtime_dir);
return ret;
}
static int mark_ce_flags(const char *path, int flag, int mark) static int mark_ce_flags(const char *path, int flag, int mark)
{ {
int namelen = strlen(path); int namelen = strlen(path);
@ -741,6 +902,7 @@ static int reupdate_callback(struct parse_opt_ctx_t *ctx,
int cmd_update_index(int argc, const char **argv, const char *prefix) int cmd_update_index(int argc, const char **argv, const char *prefix)
{ {
int newfd, entries, has_errors = 0, line_termination = '\n'; int newfd, entries, has_errors = 0, line_termination = '\n';
int untracked_cache = -1;
int read_from_stdin = 0; int read_from_stdin = 0;
int prefix_length = prefix ? strlen(prefix) : 0; int prefix_length = prefix ? strlen(prefix) : 0;
int preferred_index_format = 0; int preferred_index_format = 0;
@ -832,6 +994,10 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
N_("write index in this format")), N_("write index in this format")),
OPT_BOOL(0, "split-index", &split_index, OPT_BOOL(0, "split-index", &split_index,
N_("enable or disable split index")), N_("enable or disable split index")),
OPT_BOOL(0, "untracked-cache", &untracked_cache,
N_("enable/disable untracked cache")),
OPT_SET_INT(0, "force-untracked-cache", &untracked_cache,
N_("enable untracked cache without testing the filesystem"), 2),
OPT_END() OPT_END()
}; };
@ -938,6 +1104,28 @@ int cmd_update_index(int argc, const char **argv, const char *prefix)
the_index.split_index = NULL; the_index.split_index = NULL;
the_index.cache_changed |= SOMETHING_CHANGED; the_index.cache_changed |= SOMETHING_CHANGED;
} }
if (untracked_cache > 0) {
struct untracked_cache *uc;
if (untracked_cache < 2) {
setup_work_tree();
if (!test_if_untracked_cache_is_supported())
return 1;
}
if (!the_index.untracked) {
uc = xcalloc(1, sizeof(*uc));
strbuf_init(&uc->ident, 100);
uc->exclude_per_dir = ".gitignore";
/* should be the same flags used by git-status */
uc->dir_flags = DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES;
the_index.untracked = uc;
}
add_untracked_ident(the_index.untracked);
the_index.cache_changed |= UNTRACKED_CHANGED;
} else if (!untracked_cache && the_index.untracked) {
the_index.untracked = NULL;
the_index.cache_changed |= UNTRACKED_CHANGED;
}
if (active_cache_changed) { if (active_cache_changed) {
if (newfd < 0) { if (newfd < 0) {

View File

@ -297,8 +297,11 @@ static inline unsigned int canon_mode(unsigned int mode)
#define RESOLVE_UNDO_CHANGED (1 << 4) #define RESOLVE_UNDO_CHANGED (1 << 4)
#define CACHE_TREE_CHANGED (1 << 5) #define CACHE_TREE_CHANGED (1 << 5)
#define SPLIT_INDEX_ORDERED (1 << 6) #define SPLIT_INDEX_ORDERED (1 << 6)
#define UNTRACKED_CHANGED (1 << 7)
struct split_index; struct split_index;
struct untracked_cache;
struct index_state { struct index_state {
struct cache_entry **cache; struct cache_entry **cache;
unsigned int version; unsigned int version;
@ -312,6 +315,7 @@ struct index_state {
struct hashmap name_hash; struct hashmap name_hash;
struct hashmap dir_hash; struct hashmap dir_hash;
unsigned char sha1[20]; unsigned char sha1[20];
struct untracked_cache *untracked;
}; };
extern struct index_state the_index; extern struct index_state the_index;
@ -563,6 +567,8 @@ extern void fill_stat_data(struct stat_data *sd, struct stat *st);
* INODE_CHANGED, and DATA_CHANGED. * INODE_CHANGED, and DATA_CHANGED.
*/ */
extern int match_stat_data(const struct stat_data *sd, struct stat *st); extern int match_stat_data(const struct stat_data *sd, struct stat *st);
extern int match_stat_data_racy(const struct index_state *istate,
const struct stat_data *sd, struct stat *st);
extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st); extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st);

View File

@ -2128,3 +2128,14 @@ void mingw_startup()
/* initialize Unicode console */ /* initialize Unicode console */
winansi_init(); winansi_init();
} }
int uname(struct utsname *buf)
{
DWORD v = GetVersion();
memset(buf, 0, sizeof(*buf));
strcpy(buf->sysname, "Windows");
sprintf(buf->release, "%u.%u", v & 0xff, (v >> 8) & 0xff);
/* assuming NT variants only.. */
sprintf(buf->version, "%u", (v >> 16) & 0x7fff);
return 0;
}

View File

@ -76,6 +76,14 @@ struct itimerval {
}; };
#define ITIMER_REAL 0 #define ITIMER_REAL 0
struct utsname {
char sysname[16];
char nodename[1];
char release[16];
char version[16];
char machine[1];
};
/* /*
* sanitize preprocessor namespace polluted by Windows headers defining * sanitize preprocessor namespace polluted by Windows headers defining
* macros which collide with git local versions * macros which collide with git local versions
@ -175,6 +183,7 @@ struct passwd *getpwuid(uid_t uid);
int setitimer(int type, struct itimerval *in, struct itimerval *out); int setitimer(int type, struct itimerval *in, struct itimerval *out);
int sigaction(int sig, struct sigaction *in, struct sigaction *out); int sigaction(int sig, struct sigaction *in, struct sigaction *out);
int link(const char *oldpath, const char *newpath); int link(const char *oldpath, const char *newpath);
int uname(struct utsname *buf);
/* /*
* replacements of existing functions * replacements of existing functions

990
dir.c

File diff suppressed because it is too large Load Diff

82
dir.h
View File

@ -66,6 +66,7 @@ struct exclude_stack {
struct exclude_stack *prev; /* the struct exclude_stack for the parent directory */ struct exclude_stack *prev; /* the struct exclude_stack for the parent directory */
int baselen; int baselen;
int exclude_ix; /* index of exclude_list within EXC_DIRS exclude_list_group */ int exclude_ix; /* index of exclude_list within EXC_DIRS exclude_list_group */
struct untracked_cache_dir *ucd;
}; };
struct exclude_list_group { struct exclude_list_group {
@ -73,6 +74,73 @@ struct exclude_list_group {
struct exclude_list *el; struct exclude_list *el;
}; };
struct sha1_stat {
struct stat_data stat;
unsigned char sha1[20];
int valid;
};
/*
* Untracked cache
*
* The following inputs are sufficient to determine what files in a
* directory are excluded:
*
* - The list of files and directories of the directory in question
* - The $GIT_DIR/index
* - dir_struct flags
* - The content of $GIT_DIR/info/exclude
* - The content of core.excludesfile
* - The content (or the lack) of .gitignore of all parent directories
* from $GIT_WORK_TREE
* - The check_only flag in read_directory_recursive (for
* DIR_HIDE_EMPTY_DIRECTORIES)
*
* The first input can be checked using directory mtime. In many
* filesystems, directory mtime (stat_data field) is updated when its
* files or direct subdirs are added or removed.
*
* The second one can be hooked from cache_tree_invalidate_path().
* Whenever a file (or a submodule) is added or removed from a
* directory, we invalidate that directory.
*
* The remaining inputs are easy, their SHA-1 could be used to verify
* their contents (exclude_sha1[], info_exclude_sha1[] and
* excludes_file_sha1[])
*/
struct untracked_cache_dir {
struct untracked_cache_dir **dirs;
char **untracked;
struct stat_data stat_data;
unsigned int untracked_alloc, dirs_nr, dirs_alloc;
unsigned int untracked_nr;
unsigned int check_only : 1;
/* all data except 'dirs' in this struct are good */
unsigned int valid : 1;
unsigned int recurse : 1;
/* null SHA-1 means this directory does not have .gitignore */
unsigned char exclude_sha1[20];
char name[FLEX_ARRAY];
};
struct untracked_cache {
struct sha1_stat ss_info_exclude;
struct sha1_stat ss_excludes_file;
const char *exclude_per_dir;
struct strbuf ident;
/*
* dir_struct#flags must match dir_flags or the untracked
* cache is ignored.
*/
unsigned dir_flags;
struct untracked_cache_dir *root;
/* Statistics */
int dir_created;
int gitignore_invalidated;
int dir_invalidated;
int dir_opened;
};
struct dir_struct { struct dir_struct {
int nr, alloc; int nr, alloc;
int ignored_nr, ignored_alloc; int ignored_nr, ignored_alloc;
@ -120,6 +188,12 @@ struct dir_struct {
struct exclude_stack *exclude_stack; struct exclude_stack *exclude_stack;
struct exclude *exclude; struct exclude *exclude;
struct strbuf basebuf; struct strbuf basebuf;
/* Enable untracked file cache if set */
struct untracked_cache *untracked;
struct sha1_stat ss_info_exclude;
struct sha1_stat ss_excludes_file;
unsigned unmanaged_exclude_files;
}; };
/* /*
@ -226,4 +300,12 @@ static inline int dir_path_match(const struct dir_entry *ent,
has_trailing_dir); has_trailing_dir);
} }
void untracked_cache_invalidate_path(struct index_state *, const char *);
void untracked_cache_remove_from_index(struct index_state *, const char *);
void untracked_cache_add_to_index(struct index_state *, const char *);
void free_untracked_cache(struct untracked_cache *);
struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz);
void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked);
void add_untracked_ident(struct untracked_cache *);
#endif #endif

View File

@ -19,6 +19,7 @@
*/ */
#include "git-compat-util.h" #include "git-compat-util.h"
#include "ewok.h" #include "ewok.h"
#include "strbuf.h"
int ewah_serialize_native(struct ewah_bitmap *self, int fd) int ewah_serialize_native(struct ewah_bitmap *self, int fd)
{ {
@ -110,6 +111,18 @@ int ewah_serialize(struct ewah_bitmap *self, int fd)
return ewah_serialize_to(self, write_helper, (void *)(intptr_t)fd); return ewah_serialize_to(self, write_helper, (void *)(intptr_t)fd);
} }
static int write_strbuf(void *user_data, const void *data, size_t len)
{
struct strbuf *sb = user_data;
strbuf_add(sb, data, len);
return len;
}
int ewah_serialize_strbuf(struct ewah_bitmap *self, struct strbuf *sb)
{
return ewah_serialize_to(self, write_strbuf, sb);
}
int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len) int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len)
{ {
const uint8_t *ptr = map; const uint8_t *ptr = map;

View File

@ -30,6 +30,7 @@
# define ewah_calloc xcalloc # define ewah_calloc xcalloc
#endif #endif
struct strbuf;
typedef uint64_t eword_t; typedef uint64_t eword_t;
#define BITS_IN_WORD (sizeof(eword_t) * 8) #define BITS_IN_WORD (sizeof(eword_t) * 8)
@ -98,6 +99,7 @@ int ewah_serialize_to(struct ewah_bitmap *self,
void *out); void *out);
int ewah_serialize(struct ewah_bitmap *self, int fd); int ewah_serialize(struct ewah_bitmap *self, int fd);
int ewah_serialize_native(struct ewah_bitmap *self, int fd); int ewah_serialize_native(struct ewah_bitmap *self, int fd);
int ewah_serialize_strbuf(struct ewah_bitmap *self, struct strbuf *);
int ewah_deserialize(struct ewah_bitmap *self, int fd); int ewah_deserialize(struct ewah_bitmap *self, int fd);
int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len); int ewah_read_mmap(struct ewah_bitmap *self, const void *map, size_t len);

View File

@ -189,6 +189,7 @@
#elif defined(_MSC_VER) #elif defined(_MSC_VER)
#include "compat/msvc.h" #include "compat/msvc.h"
#else #else
#include <sys/utsname.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <sys/resource.h> #include <sys/resource.h>
#include <sys/socket.h> #include <sys/socket.h>

View File

@ -39,11 +39,12 @@ static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
#define CACHE_EXT_TREE 0x54524545 /* "TREE" */ #define CACHE_EXT_TREE 0x54524545 /* "TREE" */
#define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */ #define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */
#define CACHE_EXT_LINK 0x6c696e6b /* "link" */ #define CACHE_EXT_LINK 0x6c696e6b /* "link" */
#define CACHE_EXT_UNTRACKED 0x554E5452 /* "UNTR" */
/* changes that can be kept in $GIT_DIR/index (basically all extensions) */ /* changes that can be kept in $GIT_DIR/index (basically all extensions) */
#define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \ #define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \
CE_ENTRY_ADDED | CE_ENTRY_REMOVED | CE_ENTRY_CHANGED | \ CE_ENTRY_ADDED | CE_ENTRY_REMOVED | CE_ENTRY_CHANGED | \
SPLIT_INDEX_ORDERED) SPLIT_INDEX_ORDERED | UNTRACKED_CHANGED)
struct index_state the_index; struct index_state the_index;
static const char *alternate_index_output; static const char *alternate_index_output;
@ -79,6 +80,7 @@ void rename_index_entry_at(struct index_state *istate, int nr, const char *new_n
memcpy(new->name, new_name, namelen + 1); memcpy(new->name, new_name, namelen + 1);
cache_tree_invalidate_path(istate, old->name); cache_tree_invalidate_path(istate, old->name);
untracked_cache_remove_from_index(istate, old->name);
remove_index_entry_at(istate, nr); remove_index_entry_at(istate, nr);
add_index_entry(istate, new, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); add_index_entry(istate, new, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
} }
@ -270,20 +272,34 @@ static int ce_match_stat_basic(const struct cache_entry *ce, struct stat *st)
return changed; return changed;
} }
static int is_racy_stat(const struct index_state *istate,
const struct stat_data *sd)
{
return (istate->timestamp.sec &&
#ifdef USE_NSEC
/* nanosecond timestamped files can also be racy! */
(istate->timestamp.sec < sd->sd_mtime.sec ||
(istate->timestamp.sec == sd->sd_mtime.sec &&
istate->timestamp.nsec <= sd->sd_mtime.nsec))
#else
istate->timestamp.sec <= sd->sd_mtime.sec
#endif
);
}
static int is_racy_timestamp(const struct index_state *istate, static int is_racy_timestamp(const struct index_state *istate,
const struct cache_entry *ce) const struct cache_entry *ce)
{ {
return (!S_ISGITLINK(ce->ce_mode) && return (!S_ISGITLINK(ce->ce_mode) &&
istate->timestamp.sec && is_racy_stat(istate, &ce->ce_stat_data));
#ifdef USE_NSEC }
/* nanosecond timestamped files can also be racy! */
(istate->timestamp.sec < ce->ce_stat_data.sd_mtime.sec || int match_stat_data_racy(const struct index_state *istate,
(istate->timestamp.sec == ce->ce_stat_data.sd_mtime.sec && const struct stat_data *sd, struct stat *st)
istate->timestamp.nsec <= ce->ce_stat_data.sd_mtime.nsec)) {
#else if (is_racy_stat(istate, sd))
istate->timestamp.sec <= ce->ce_stat_data.sd_mtime.sec return MTIME_CHANGED;
#endif return match_stat_data(sd, st);
);
} }
int ie_match_stat(const struct index_state *istate, int ie_match_stat(const struct index_state *istate,
@ -538,6 +554,7 @@ int remove_file_from_index(struct index_state *istate, const char *path)
if (pos < 0) if (pos < 0)
pos = -pos-1; pos = -pos-1;
cache_tree_invalidate_path(istate, path); cache_tree_invalidate_path(istate, path);
untracked_cache_remove_from_index(istate, path);
while (pos < istate->cache_nr && !strcmp(istate->cache[pos]->name, path)) while (pos < istate->cache_nr && !strcmp(istate->cache[pos]->name, path))
remove_index_entry_at(istate, pos); remove_index_entry_at(istate, pos);
return 0; return 0;
@ -982,6 +999,8 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e
} }
pos = -pos-1; pos = -pos-1;
untracked_cache_add_to_index(istate, ce->name);
/* /*
* Inserting a merged entry ("stage 0") into the index * Inserting a merged entry ("stage 0") into the index
* will always replace all non-merged entries.. * will always replace all non-merged entries..
@ -1372,6 +1391,9 @@ static int read_index_extension(struct index_state *istate,
if (read_link_extension(istate, data, sz)) if (read_link_extension(istate, data, sz))
return -1; return -1;
break; break;
case CACHE_EXT_UNTRACKED:
istate->untracked = read_untracked_extension(data, sz);
break;
default: default:
if (*ext < 'A' || 'Z' < *ext) if (*ext < 'A' || 'Z' < *ext)
return error("index uses %.4s extension, which we do not understand", return error("index uses %.4s extension, which we do not understand",
@ -1667,6 +1689,8 @@ int discard_index(struct index_state *istate)
istate->cache = NULL; istate->cache = NULL;
istate->cache_alloc = 0; istate->cache_alloc = 0;
discard_split_index(istate); discard_split_index(istate);
free_untracked_cache(istate->untracked);
istate->untracked = NULL;
return 0; return 0;
} }
@ -2053,6 +2077,17 @@ static int do_write_index(struct index_state *istate, int newfd,
if (err) if (err)
return -1; return -1;
} }
if (!strip_extensions && istate->untracked) {
struct strbuf sb = STRBUF_INIT;
write_untracked_extension(&sb, istate->untracked);
err = write_index_ext_header(&c, newfd, CACHE_EXT_UNTRACKED,
sb.len) < 0 ||
ce_write(&c, newfd, sb.buf, sb.len) < 0;
strbuf_release(&sb);
if (err)
return -1;
}
if (ce_flush(&c, newfd, istate->sha1) || fstat(newfd, &st)) if (ce_flush(&c, newfd, istate->sha1) || fstat(newfd, &st))
return -1; return -1;

View File

@ -41,13 +41,6 @@ int read_link_extension(struct index_state *istate,
return 0; return 0;
} }
static int write_strbuf(void *user_data, const void *data, size_t len)
{
struct strbuf *sb = user_data;
strbuf_add(sb, data, len);
return len;
}
int write_link_extension(struct strbuf *sb, int write_link_extension(struct strbuf *sb,
struct index_state *istate) struct index_state *istate)
{ {
@ -55,8 +48,8 @@ int write_link_extension(struct strbuf *sb,
strbuf_add(sb, si->base_sha1, 20); strbuf_add(sb, si->base_sha1, 20);
if (!si->delete_bitmap && !si->replace_bitmap) if (!si->delete_bitmap && !si->replace_bitmap)
return 0; return 0;
ewah_serialize_to(si->delete_bitmap, write_strbuf, sb); ewah_serialize_strbuf(si->delete_bitmap, sb);
ewah_serialize_to(si->replace_bitmap, write_strbuf, sb); ewah_serialize_strbuf(si->replace_bitmap, sb);
return 0; return 0;
} }

353
t/t7063-status-untracked-cache.sh Executable file
View File

@ -0,0 +1,353 @@
#!/bin/sh
test_description='test untracked cache'
. ./test-lib.sh
avoid_racy() {
sleep 1
}
git update-index --untracked-cache
# It's fine if git update-index returns an error code other than one,
# it'll be caught in the first test.
if test $? -eq 1; then
skip_all='This system does not support untracked cache'
test_done
fi
test_expect_success 'setup' '
git init worktree &&
cd worktree &&
mkdir done dtwo dthree &&
touch one two three done/one dtwo/two dthree/three &&
git add one two done/one &&
: >.git/info/exclude &&
git update-index --untracked-cache
'
test_expect_success 'untracked cache is empty' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 0000000000000000000000000000000000000000
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
EOF
test_cmp ../expect ../actual
'
cat >../status.expect <<EOF &&
A done/one
A one
A two
?? dthree/
?? dtwo/
?? three
EOF
cat >../dump.expect <<EOF &&
info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ 0000000000000000000000000000000000000000 recurse valid
dthree/
dtwo/
three
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
three
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_expect_success 'status first time (empty cache)' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 3
gitignore invalidation: 1
directory invalidation: 0
opendir: 4
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'untracked cache after first status' '
test-dump-untracked-cache >../actual &&
test_cmp ../dump.expect ../actual
'
test_expect_success 'status second time (fully populated cache)' '
avoid_racy &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 0
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'untracked cache after second status' '
test-dump-untracked-cache >../actual &&
test_cmp ../dump.expect ../actual
'
test_expect_success 'modify in root directory, one dir invalidation' '
avoid_racy &&
: >four &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
A done/one
A one
A two
?? dthree/
?? dtwo/
?? four
?? three
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 1
opendir: 1
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ 0000000000000000000000000000000000000000 recurse valid
dthree/
dtwo/
four
three
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
three
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'new .gitignore invalidates recursively' '
avoid_racy &&
echo four >.gitignore &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
A done/one
A one
A two
?? .gitignore
?? dthree/
?? dtwo/
?? three
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 1
directory invalidation: 1
opendir: 4
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dthree/
dtwo/
three
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
three
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'new info/exclude invalidates everything' '
avoid_racy &&
echo three >>.git/info/exclude &&
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
A done/one
A one
A two
?? .gitignore
?? dtwo/
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 1
directory invalidation: 0
opendir: 4
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'move two from tracked to untracked' '
git rm --cached two &&
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'status after the move' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
A done/one
A one
?? .gitignore
?? dtwo/
?? two
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 1
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
two
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'move two from untracked to tracked' '
git add two &&
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_expect_success 'status after the move' '
: >../trace &&
GIT_TRACE_UNTRACKED_STATS="$TRASH_DIRECTORY/trace" \
git status --porcelain >../actual &&
cat >../status.expect <<EOF &&
A done/one
A one
A two
?? .gitignore
?? dtwo/
EOF
test_cmp ../status.expect ../actual &&
cat >../trace.expect <<EOF &&
node creation: 0
gitignore invalidation: 0
directory invalidation: 0
opendir: 1
EOF
test_cmp ../trace.expect ../trace
'
test_expect_success 'verify untracked cache dump' '
test-dump-untracked-cache >../actual &&
cat >../expect <<EOF &&
info/exclude 13263c0978fb9fad16b2d580fb800b6d811c3ff0
core.excludesfile 0000000000000000000000000000000000000000
exclude_per_dir .gitignore
flags 00000006
/ e6fcc8f2ee31bae321d66afd183fcb7237afae6e recurse valid
.gitignore
dtwo/
/done/ 0000000000000000000000000000000000000000 recurse valid
/dthree/ 0000000000000000000000000000000000000000 recurse check_only valid
/dtwo/ 0000000000000000000000000000000000000000 recurse check_only valid
two
EOF
test_cmp ../expect ../actual
'
test_done

View File

@ -0,0 +1,62 @@
#include "cache.h"
#include "dir.h"
static int compare_untracked(const void *a_, const void *b_)
{
const char *const *a = a_;
const char *const *b = b_;
return strcmp(*a, *b);
}
static int compare_dir(const void *a_, const void *b_)
{
const struct untracked_cache_dir *const *a = a_;
const struct untracked_cache_dir *const *b = b_;
return strcmp((*a)->name, (*b)->name);
}
static void dump(struct untracked_cache_dir *ucd, struct strbuf *base)
{
int i, len;
qsort(ucd->untracked, ucd->untracked_nr, sizeof(*ucd->untracked),
compare_untracked);
qsort(ucd->dirs, ucd->dirs_nr, sizeof(*ucd->dirs),
compare_dir);
len = base->len;
strbuf_addf(base, "%s/", ucd->name);
printf("%s %s", base->buf,
sha1_to_hex(ucd->exclude_sha1));
if (ucd->recurse)
fputs(" recurse", stdout);
if (ucd->check_only)
fputs(" check_only", stdout);
if (ucd->valid)
fputs(" valid", stdout);
printf("\n");
for (i = 0; i < ucd->untracked_nr; i++)
printf("%s\n", ucd->untracked[i]);
for (i = 0; i < ucd->dirs_nr; i++)
dump(ucd->dirs[i], base);
strbuf_setlen(base, len);
}
int main(int ac, char **av)
{
struct untracked_cache *uc;
struct strbuf base = STRBUF_INIT;
setup_git_directory();
if (read_cache() < 0)
die("unable to read index file");
uc = the_index.untracked;
if (!uc) {
printf("no untracked cache\n");
return 0;
}
printf("info/exclude %s\n", sha1_to_hex(uc->ss_info_exclude.sha1));
printf("core.excludesfile %s\n", sha1_to_hex(uc->ss_excludes_file.sha1));
printf("exclude_per_dir %s\n", uc->exclude_per_dir);
printf("flags %08x\n", uc->dir_flags);
if (uc->root)
dump(uc->root, &base);
return 0;
}

View File

@ -9,6 +9,7 @@
#include "refs.h" #include "refs.h"
#include "attr.h" #include "attr.h"
#include "split-index.h" #include "split-index.h"
#include "dir.h"
/* /*
* Error messages expected by scripts out of plumbing commands such as * Error messages expected by scripts out of plumbing commands such as
@ -1259,8 +1260,10 @@ static int verify_uptodate_sparse(const struct cache_entry *ce,
static void invalidate_ce_path(const struct cache_entry *ce, static void invalidate_ce_path(const struct cache_entry *ce,
struct unpack_trees_options *o) struct unpack_trees_options *o)
{ {
if (ce) if (!ce)
cache_tree_invalidate_path(o->src_index, ce->name); return;
cache_tree_invalidate_path(o->src_index, ce->name);
untracked_cache_invalidate_path(o->src_index, ce->name);
} }
/* /*

View File

@ -585,6 +585,8 @@ static void wt_status_collect_untracked(struct wt_status *s)
DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES; DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES;
if (s->show_ignored_files) if (s->show_ignored_files)
dir.flags |= DIR_SHOW_IGNORED_TOO; dir.flags |= DIR_SHOW_IGNORED_TOO;
else
dir.untracked = the_index.untracked;
setup_standard_excludes(&dir); setup_standard_excludes(&dir);
fill_directory(&dir, &s->pathspec); fill_directory(&dir, &s->pathspec);