* support '.noupdate' -- similar to '.noindex', maildirs containing

'.noupdate' will be ignored; however, they will _not_ be ignored when doing
  a full update (--rebuild)
This commit is contained in:
djcb 2012-05-22 10:19:49 +03:00
parent 5d5533c953
commit 9367f1ac6d
8 changed files with 208 additions and 125 deletions

View File

@ -309,7 +309,6 @@ mu_index_set_xbatch_size (MuIndex *index, guint xbatchsize)
MuError
mu_index_run (MuIndex *index, const char* path,
gboolean reindex, MuIndexStats *stats,
@ -337,6 +336,7 @@ mu_index_run (MuIndex *index, const char* path,
rv = mu_maildir_walk (path,
(MuMaildirWalkMsgCallback)on_run_maildir_msg,
(MuMaildirWalkDirCallback)on_run_maildir_dir,
reindex, /* re-index, ie. do a full update */
&cb_data);
mu_store_flush (index->_store);
@ -393,7 +393,7 @@ mu_index_stats (MuIndex *index, const char* path,
return mu_maildir_walk (path,
(MuMaildirWalkMsgCallback)on_stats_maildir_file,
NULL,&cb_data);
NULL, FALSE, &cb_data);
}
struct _CleanupData {

View File

@ -1,7 +1,7 @@
/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/
/*
** Copyright (C) 2008-2011 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2008-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
@ -47,6 +47,8 @@
#include "mu-str.h"
#define MU_MAILDIR_NOINDEX_FILE ".noindex"
#define MU_MAILDIR_NOUPDATE_FILE ".noupdate"
/* On Linux (and some BSD), we have entry->d_type, but some file
* systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN.
@ -88,13 +90,11 @@ create_maildir (const char *path, mode_t mode, GError **err)
/* note, g_mkdir_with_parents won't detect an error if
* there's already such a dir, but with the wrong
* permissions; so we need to check */
if (rv != 0 || !mu_util_check_dir(fullpath, TRUE, TRUE)) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_MKDIR,
"creating dir failed for %s: %s",
fullpath,
strerror (errno));
return FALSE;
}
if (rv != 0 || !mu_util_check_dir(fullpath, TRUE, TRUE))
return mu_util_g_set_error
(err,MU_ERROR_FILE_CANNOT_MKDIR,
"creating dir failed for %s: %s",
fullpath, strerror (errno));
}
return TRUE;
@ -114,13 +114,10 @@ create_noindex (const char *path, GError **err)
/* note, if the 'close' failed, creation may still have
* succeeded...*/
if (fd < 0 || close (fd) != 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_CREATE,
"error in create_noindex: %s",
strerror (errno));
return FALSE;
}
if (fd < 0 || close (fd) != 0)
return mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_CREATE,
"error in create_noindex: %s",
strerror (errno));
return TRUE;
}
@ -146,22 +143,22 @@ mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, GError **err)
static gboolean
check_subdir (const char *src, gboolean *in_cur, GError **err)
{
gboolean rv;
gchar *srcpath;
srcpath = g_path_get_dirname (src);
rv = TRUE;
if (g_str_has_suffix (srcpath, "new"))
*in_cur = FALSE;
else if (g_str_has_suffix (srcpath, "cur"))
*in_cur = TRUE;
else {
g_set_error(err, 0, MU_ERROR_FILE_INVALID_SOURCE,
"invalid source message '%s'", src);
return FALSE;
}
else
rv = mu_util_g_set_error(err, MU_ERROR_FILE_INVALID_SOURCE,
"invalid source message '%s'", src);
g_free (srcpath);
return TRUE;
return rv;
}
static gchar*
@ -207,23 +204,21 @@ mu_maildir_link (const char* src, const char *targetpath, GError **err)
rv = symlink (src, targetfullpath);
if (rv != 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_LINK,
"error creating link %s => %s: %s",
targetfullpath, src, strerror (errno));
g_free (targetfullpath);
return FALSE;
}
if (rv != 0)
mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_LINK,
"error creating link %s => %s: %s",
targetfullpath, src, strerror (errno));
g_free (targetfullpath);
return TRUE;
return rv == 0 ? TRUE: FALSE;
}
static MuError
process_dir (const char* path, const gchar *mdir,
MuMaildirWalkMsgCallback msg_cb,
MuMaildirWalkDirCallback dir_cb, void *data);
MuMaildirWalkDirCallback dir_cb, gboolean full,
void *data);
static MuError
process_file (const char* fullpath, const gchar* mdir,
@ -292,26 +287,26 @@ is_maildir_new_or_cur (const char *path)
return FALSE;
}
/* check if there is a noindex file (MU_MAILDIR_NOINDEX_FILE) in this
/* check if there path contains file; used for checking if there is
* MU_MAILDIR_NOINDEX_FILE or MU_MAILDIR_NOUPDATE_FILE in this
* dir; */
static gboolean
has_noindex_file (const char *path)
dir_contains_file (const char *path, const char *file)
{
const char* noindexpath;
const char* fullpath;
/* static buffer */
noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE);
fullpath = mu_str_fullpath_s (path, file);
if (access (noindexpath, F_OK) == 0)
if (access (fullpath, F_OK) == 0)
return TRUE;
else if (G_UNLIKELY(errno != ENOENT))
g_warning ("error testing for noindex file %s: %s",
noindexpath, strerror(errno));
g_warning ("error testing for %s/%s: %s",
fullpath, file, strerror(errno));
return FALSE;
}
static gboolean
is_dotdir_to_ignore (const char* dir)
{
@ -351,6 +346,9 @@ ignore_dir_entry (struct dirent *entry, unsigned char d_type)
if (entry->d_name[0] == 'd' &&
strncmp (entry->d_name, "dovecot", 7) == 0)
return TRUE;
/* ignore special files */
if (entry->d_name[0] == '.')
return TRUE;
/* ignore core files */
if (entry->d_name[0] == 'c' &&
strncmp (entry->d_name, "core", 4) == 0)
@ -390,7 +388,7 @@ static MuError
process_dir_entry (const char* path, const char* mdir, struct dirent *entry,
MuMaildirWalkMsgCallback cb_msg,
MuMaildirWalkDirCallback cb_dir,
void *data)
gboolean full, void *data)
{
const char *fp;
char* fullpath;
@ -420,10 +418,9 @@ process_dir_entry (const char* path, const char* mdir, struct dirent *entry,
MuError rv;
/* my_mdir is the search maildir (the dir starting
* with the top-level maildir as /, and without the
* /tmp, /cur, /new
*/
* /tmp, /cur, /new */
my_mdir = get_mdir_for_path (mdir, entry->d_name);
rv = process_dir (fullpath, my_mdir, cb_msg, cb_dir, data);
rv = process_dir (fullpath, my_mdir, cb_msg, cb_dir, full, data);
g_free (my_mdir);
return rv;
@ -470,7 +467,8 @@ dirent_cmp (struct dirent *d1, struct dirent *d2)
static MuError
process_dir_entries (DIR *dir, const char* path, const char* mdir,
MuMaildirWalkMsgCallback msg_cb,
MuMaildirWalkDirCallback dir_cb, void *data)
MuMaildirWalkDirCallback dir_cb,
gboolean full, void *data)
{
MuError result;
GSList *lst, *c;
@ -502,7 +500,7 @@ process_dir_entries (DIR *dir, const char* path, const char* mdir,
for (c = lst, result = MU_OK; c && result == MU_OK; c = g_slist_next(c))
result = process_dir_entry (path, mdir, (struct dirent*)c->data,
msg_cb, dir_cb, data);
msg_cb, dir_cb, full, data);
g_slist_foreach (lst, (GFunc)dirent_destroy, NULL);
g_slist_free (lst);
@ -513,22 +511,22 @@ process_dir_entries (DIR *dir, const char* path, const char* mdir,
static MuError
process_dir (const char* path, const char* mdir,
MuMaildirWalkMsgCallback msg_cb,
MuMaildirWalkDirCallback dir_cb, void *data)
MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb,
gboolean full, void *data)
{
MuError result;
DIR* dir;
/* if it has a noindex file, we ignore this dir */
if (has_noindex_file (path)) {
g_debug ("found .noindex: ignoring dir %s", path);
if (dir_contains_file (path, MU_MAILDIR_NOINDEX_FILE) ||
(!full && dir_contains_file (path, MU_MAILDIR_NOUPDATE_FILE))) {
g_debug ("found noindex/noupdate: ignoring dir %s", path);
return MU_OK;
}
dir = opendir (path);
if (G_UNLIKELY(!dir)) {
g_warning ("%s: ignoring %s: %s", __FUNCTION__,
path, strerror(errno));
g_warning ("opendir failed %s: %s", path, strerror(errno));
return MU_OK;
}
@ -541,7 +539,7 @@ process_dir (const char* path, const char* mdir,
}
}
result = process_dir_entries (dir, path, mdir, msg_cb, dir_cb, data);
result = process_dir_entries (dir, path, mdir, msg_cb, dir_cb, full, data);
closedir (dir);
/* only run dir_cb if it exists and so far, things went ok */
@ -554,7 +552,8 @@ process_dir (const char* path, const char* mdir,
MuError
mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg,
MuMaildirWalkDirCallback cb_dir, void *data)
MuMaildirWalkDirCallback cb_dir, gboolean full,
void *data)
{
MuError rv;
char *mypath;
@ -567,7 +566,7 @@ mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg,
if (mypath[strlen(mypath)-1] == G_DIR_SEPARATOR)
mypath[strlen(mypath)-1] = '\0';
rv = process_dir (mypath, NULL, cb_msg, cb_dir, data);
rv = process_dir (mypath, NULL, cb_msg, cb_dir, full, data);
g_free (mypath);
return rv;
@ -617,8 +616,8 @@ clear_links (const gchar* dirname, DIR *dir, GError **err)
}
if (errno != 0)
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
"file error: %s", strerror(errno));
mu_util_g_set_error (err, MU_ERROR_FILE,
"file error: %s", strerror(errno));
return (rv == FALSE && errno == 0);
}
@ -633,12 +632,10 @@ mu_maildir_clear_links (const gchar* path, GError **err)
g_return_val_if_fail (path, FALSE);
dir = opendir (path);
if (!dir) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_OPEN,
"failed to open %s: %s", path,
strerror(errno));
return FALSE;
}
if (!dir)
return mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_OPEN,
"failed to open %s: %s", path,
strerror(errno));
rv = clear_links (path, dir, err);
closedir (dir);
@ -790,29 +787,23 @@ mu_maildir_get_new_path (const char *oldpath, const char *new_mdir,
static gboolean
msg_move_check_pre (const gchar *src, const gchar *dst, GError **err)
{
if (!g_path_is_absolute(src)) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
"source is not an absolute path: '%s'", src);
return FALSE;
}
if (!g_path_is_absolute(src))
return mu_util_g_set_error
(err, MU_ERROR_FILE,
"source is not an absolute path: '%s'", src);
if (!g_path_is_absolute(dst)) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
"target is not an absolute path: '%s'", dst);
return FALSE;
}
if (!g_path_is_absolute(dst))
return mu_util_g_set_error
(err, MU_ERROR_FILE,
"target is not an absolute path: '%s'", dst);
if (access (src, R_OK) != 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, "cannot read %s",
src);
return FALSE;
}
if (access (src, R_OK) != 0)
return mu_util_g_set_error (err, MU_ERROR_FILE,
"cannot read %s", src);
if (access (dst, F_OK) == 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, "%s already exists",
dst);
return FALSE;
}
if (access (dst, F_OK) == 0)
return mu_util_g_set_error (err, MU_ERROR_FILE,
"%s already exists", dst);
return TRUE;
}
@ -821,17 +812,13 @@ static gboolean
msg_move_check_post (const char *src, const char *dst, GError **err)
{
/* double check -- is the target really there? */
if (access (dst, F_OK) != 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, "can't find target (%s)",
dst);
return FALSE;
}
if (access (dst, F_OK) != 0)
return mu_util_g_set_error
(err, MU_ERROR_FILE, "can't find target (%s)", dst);
if (access (src, F_OK) == 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, "source is still there (%s)",
src);
return FALSE;
}
if (access (src, F_OK) == 0)
return mu_util_g_set_error
(err, MU_ERROR_FILE, "source still there (%s)", src);
return TRUE;
}
@ -843,16 +830,11 @@ msg_move (const char* src, const char *dst, GError **err)
if (!msg_move_check_pre (src, dst, err))
return FALSE;
if (rename (src, dst) != 0) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, "error moving %s to %s",
src, dst);
return FALSE;
}
if (rename (src, dst) != 0)
return mu_util_g_set_error
(err, MU_ERROR_FILE,"error moving %s to %s", src, dst);
if (!msg_move_check_post (src, dst, err))
return FALSE;
return TRUE;
return msg_move_check_post (src, dst, err);
}
gchar*
@ -869,18 +851,17 @@ mu_maildir_move_message (const char* oldpath, const char* targetmdir,
newfullpath = mu_maildir_get_new_path (oldpath, targetmdir,
newflags);
if (!newfullpath) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE,
"failed to determine target full path");
return FALSE;
mu_util_g_set_error (err, MU_ERROR_FILE,
"failed to determine targetpath");
return NULL;
}
src_is_target = (g_strcmp0 (oldpath, newfullpath) == 0);
if (!ignore_dups && src_is_target) {
g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_TARGET_EQUALS_SOURCE,
"target equals source");
return FALSE;
mu_util_g_set_error (err, MU_ERROR_FILE_TARGET_EQUALS_SOURCE,
"target equals source");
return NULL;
}
if (!src_is_target) {

View File

@ -96,7 +96,9 @@ typedef MuError (*MuMaildirWalkDirCallback)
* dotdirs are visited (ie. '.dotdir/cur'), so this enables Maildir++.
* (http://www.inter7.com/courierimap/README.maildirquota.html, search
* for 'Mission statement'). In addition, dirs containing a file named
* '.noindex' are ignored, as are their subdirectories.
* '.noindex' are ignored, as are their subdirectories, and dirs
* containing a file called '.noupdate' are ignored, unless @param
* full is TRUE.
*
* mu_walk_maildir stops if the callbacks return something different
* from MU_OK. For example, it can return MU_STOP to stop the scan, or
@ -105,6 +107,7 @@ typedef MuError (*MuMaildirWalkDirCallback)
* @param path the maildir path to scan
* @param cb_msg the callback function called for each msg
* @param cb_dir the callback function called for each dir
* @param full whether do a full scan, i.e., to ignore .noupdate files
* @param data user data pointer
*
* @return a scanner result; MU_OK if everything went ok,
@ -112,7 +115,8 @@ typedef MuError (*MuMaildirWalkDirCallback)
* case of error
*/
MuError mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg,
MuMaildirWalkDirCallback cb_dir, void *data);
MuMaildirWalkDirCallback cb_dir, gboolean full,
void *data);
/**
* recursively delete all the symbolic links in a directory tree
*

View File

@ -441,7 +441,7 @@ mu_util_fputs_encoded (const char *str, FILE *stream)
void
gboolean
mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
{
va_list ap;
@ -459,6 +459,8 @@ mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
g_set_error (err, MU_ERROR_DOMAIN, errcode, "%s", msg);
g_free (msg);
return FALSE;
}

View File

@ -439,13 +439,15 @@ typedef enum _MuError MuError;
/**
* set an error if it's not already set
* set an error if it's not already set, and return FALSE
*
* @param err errptr, or NULL
* @param errcode error code
* @param frm printf-style format, followed by paremeters
*
* @return FALSE
*/
void mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
gboolean mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...)
G_GNUC_PRINTF(3,4);

View File

@ -256,6 +256,7 @@ test_mu_maildir_walk_01 (void)
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
TRUE,
&data);
g_assert_cmpuint (MU_OK, ==, rv);
@ -269,7 +270,7 @@ test_mu_maildir_walk_01 (void)
static void
test_mu_maildir_walk_02 (void)
test_mu_maildir_walk (void)
{
char *tmpdir, *cmd, *dir;
WalkData data;
@ -287,13 +288,13 @@ test_mu_maildir_walk_02 (void)
cmd = g_strdup_printf ("touch %s%c.noindex", dir, G_DIR_SEPARATOR);
g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
g_free (cmd);
g_free (dir);
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
TRUE,
&data);
g_assert_cmpuint (MU_OK, ==, rv);
@ -305,6 +306,90 @@ test_mu_maildir_walk_02 (void)
g_free (tmpdir);
}
static void
test_mu_maildir_walk_with_noupdate (void)
{
char *tmpdir, *cmd, *dir;
WalkData data;
MuError rv;
tmpdir = copy_test_data ();
/* mark the 'new' dir with '.noindex', to ignore it */
dir = g_strdup_printf ("%s%ctestdir%cnew", tmpdir,
G_DIR_SEPARATOR, G_DIR_SEPARATOR);
cmd = g_strdup_printf ("chmod 700 %s", dir);
g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
g_free (cmd);
memset (&data, 0, sizeof(WalkData));
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
FALSE, /* ie., non-full update */
&data);
g_assert_cmpuint (MU_OK, ==, rv);
g_assert_cmpuint (data._file_count, ==, 17);
g_assert_cmpuint (data._dir_entered,==, 5);
g_assert_cmpuint (data._dir_left,==, 5);
/* again, full update. results should be the same, since there
* is no noupdate yet */
memset (&data, 0, sizeof(WalkData));
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
TRUE, /* ie., full update */
&data);
g_assert_cmpuint (MU_OK, ==, rv);
g_assert_cmpuint (data._file_count, ==, 17);
g_assert_cmpuint (data._dir_entered,==, 5);
g_assert_cmpuint (data._dir_left,==, 5);
/* add a '.noupdate' file; this affects the outcome when the
* 4th arg to mu_maildir_walk is FALSE */
cmd = g_strdup_printf ("touch %s%c.noupdate", dir, G_DIR_SEPARATOR);
g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL));
g_free (cmd);
memset (&data, 0, sizeof(WalkData));
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
FALSE, /* non-full update */
&data);
g_assert_cmpuint (MU_OK, ==, rv);
g_assert_cmpuint (data._file_count, ==, 13);
g_assert_cmpuint (data._dir_entered,==, 4);
g_assert_cmpuint (data._dir_left,==, 4);
/* now run again, but do a full update */
memset (&data, 0, sizeof(WalkData));
rv = mu_maildir_walk (tmpdir,
(MuMaildirWalkMsgCallback)msg_cb,
(MuMaildirWalkDirCallback)dir_cb,
TRUE, /* full update */
&data);
g_assert_cmpuint (MU_OK, ==, rv);
g_assert_cmpuint (data._file_count, ==, 17);
g_assert_cmpuint (data._dir_entered,==, 5);
g_assert_cmpuint (data._dir_left,==, 5);
g_free (dir);
g_free (tmpdir);
}
static void
@ -478,8 +563,10 @@ main (int argc, char *argv[])
/* mu_util_maildir_walk */
g_test_add_func ("/mu-maildir/mu-maildir-walk-01",
test_mu_maildir_walk_01);
g_test_add_func ("/mu-maildir/mu-maildir-walk-02",
test_mu_maildir_walk_02);
g_test_add_func ("/mu-maildir/mu-maildir-walk",
test_mu_maildir_walk);
g_test_add_func ("/mu-maildir/mu-maildir-walk-with-noupdate",
test_mu_maildir_walk_with_noupdate);
/* get/set flags */
g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01",

View File

@ -1,4 +1,4 @@
.TH MU-EASY 1 "January 2012" "User Manuals"
.TH MU-EASY 1 "May 2012" "User Manuals"
.SH NAME
@ -45,7 +45,8 @@ Normally, \fBmu index\fR visits all the directories under the top-level
Maildir; however, you can exclude certain directories (say, the 'trash'
or 'spam' folders) by creating a file called \fI.noindex\fR in the directory.
When \fBmu\fR sees such a file, it will exclude this directory and its
sub-directories from indexing.
sub-directories from indexing. Also see \fB.noupdate\fR in the \fBmu-index\fR
manpage.
.SH SEARCHING YOUR E-MAIL

View File

@ -1,6 +1,6 @@
.TH MU-INDEX 1 "May 2011" "User Manuals"
.TH MU-INDEX 1 "May 2012" "User Manuals"
.SH NAME
.SH NAME
mu index \- index e-mail messages stored in Maildirs
@ -14,7 +14,7 @@ mu index \- index e-mail messages stored in Maildirs
directories and storing the results in a Xapian database. The data can then be
queried using
.BR mu-find(1)
\.
\.
.B index
understands Maildirs as defined by Daniel Bernstein for qmail(7). In addition,
@ -33,6 +33,12 @@ directory and all of its subdirectories will be ignored. This can be useful to
exclude certain directories from the indexing process, for example directories
with spam-messages.
If there is a file called \fI.noupdate\fR in a directory, the contents of that
directory and all of its subdirectories will be ignored, unless we do a full
rebuild (with \fB--rebuild\fR). This can be useful to speed up things you have
some maildirs that never change. Note that you can still search for these
messages, this only affects updating the database.
The first run of \fBmu index\fR may take a few minutes if you have a lot of
mail (ten thousands of messages). Fortunately, such a full scan needs to be
done only once; after that it suffices to index the changes, which goes much