Message ID | pull.1187.v2.git.1648516027925.gitgitgadget@gmail.com (mailing list archive) |
---|---|
State | Accepted |
Commit | b7f9130a06a9960144bb9c145e62dac792328c20 |
Headers | show |
Series | [v2] mv: refresh stat info for moved entry | expand |
On 3/28/2022 9:07 PM, Victoria Dye via GitGitGadget wrote: > From: Victoria Dye <vdye@github.com> > > Update the stat info of the moved index entry in 'rename_index_entry_at()' > if the entry is up-to-date with the index. Internally, 'git mv' uses > 'rename_index_entry_at()' to move the source index entry to the destination. > However, it directly copies the stat info of the original cache entry, which > will not reflect the 'ctime' of the file renaming operation that happened as > part of the move. If a file is otherwise up-to-date with the index, that > difference in 'ctime' will make the entry appear out-of-date until the next > index-refreshing operation (e.g., 'git status'). > > Some commands, such as 'git reset', use the cached stat information to > determine whether a file is up-to-date; if this information is incorrect, > the command will fail when it should pass. In order to ensure a moved entry > is evaluated as 'up-to-date' when appropriate, refresh the destination index > entry's stat info in 'git mv' if and only if the file is up-to-date. > > Note that the test added in 't7001-mv.sh' requires a "sleep 1" to ensure the > 'ctime' of the file creation will be definitively older than the 'ctime' of > the renamed file in 'git mv'. Unfortunate that this is necessary, but it seems to be the only way to handle this because of the interaction with the system clock and the filesystem. There are several sleeps like this in t1701-racy-split-index.sh, for example. > void rename_index_entry_at(struct index_state *istate, int nr, const char *new_name) > { > - struct cache_entry *old_entry = istate->cache[nr], *new_entry; > + struct cache_entry *old_entry = istate->cache[nr], *new_entry, *refreshed; > int namelen = strlen(new_name); > > new_entry = make_empty_cache_entry(istate, namelen); > @@ -147,7 +147,20 @@ void rename_index_entry_at(struct index_state *istate, int nr, const char *new_n > cache_tree_invalidate_path(istate, old_entry->name); > untracked_cache_remove_from_index(istate, old_entry->name); > remove_index_entry_at(istate, nr); > - add_index_entry(istate, new_entry, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); > + > + /* > + * Refresh the new index entry. Using 'refresh_cache_entry' ensures > + * we only update stat info if the entry is otherwise up-to-date (i.e., > + * the contents/mode haven't changed). This ensures that we reflect the > + * 'ctime' of the rename in the index without (incorrectly) updating > + * the cached stat info to reflect unstaged changes on disk. > + */ > + refreshed = refresh_cache_entry(istate, new_entry, CE_MATCH_REFRESH); > + if (refreshed && refreshed != new_entry) { > + add_index_entry(istate, refreshed, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); > + discard_cache_entry(new_entry); I'm glad you're checking that both refreshed is non-NULL and not equal to new_entry. Both are possible results from refresh_cache_entry(). > +test_expect_success 'mv -f refreshes updated index entry' ' > + echo test >bar && > + git add bar && > + git commit -m test && > + > + echo foo >foo && > + git add foo && > + > + # Wait one second to ensure ctime of rename will differ from original > + # file creation ctime. > + sleep 1 && > + git mv -f foo bar && > + git reset --merge HEAD && > + > + # Verify the index has been reset > + git diff-files >out && > + test_must_be_empty out > +' > + New test looks good. Thanks, -Stolee
Derrick Stolee <derrickstolee@github.com> writes: >> Note that the test added in 't7001-mv.sh' requires a "sleep 1" to ensure the >> 'ctime' of the file creation will be definitively older than the 'ctime' of >> the renamed file in 'git mv'. > > Unfortunate that this is necessary, but it seems to be the only way > to handle this because of the interaction with the system clock and > the filesystem. There are several sleeps like this in > t1701-racy-split-index.sh, for example. Does "test-tool chmtime" to tweak the filesystem timestamp help? I didn't look at the specific step that uses sleep to work around. >> + >> + /* >> + * Refresh the new index entry. Using 'refresh_cache_entry' ensures >> + * we only update stat info if the entry is otherwise up-to-date (i.e., >> + * the contents/mode haven't changed). This ensures that we reflect the >> + * 'ctime' of the rename in the index without (incorrectly) updating >> + * the cached stat info to reflect unstaged changes on disk. >> + */ >> + refreshed = refresh_cache_entry(istate, new_entry, CE_MATCH_REFRESH); >> + if (refreshed && refreshed != new_entry) { >> + add_index_entry(istate, refreshed, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); >> + discard_cache_entry(new_entry); > > I'm glad you're checking that both refreshed is non-NULL and not equal > to new_entry. Both are possible results from refresh_cache_entry(). ;-)
On 3/29/2022 12:44 PM, Junio C Hamano wrote: > Derrick Stolee <derrickstolee@github.com> writes: > >>> Note that the test added in 't7001-mv.sh' requires a "sleep 1" to ensure the >>> 'ctime' of the file creation will be definitively older than the 'ctime' of >>> the renamed file in 'git mv'. >> >> Unfortunate that this is necessary, but it seems to be the only way >> to handle this because of the interaction with the system clock and >> the filesystem. There are several sleeps like this in >> t1701-racy-split-index.sh, for example. > > Does "test-tool chmtime" to tweak the filesystem timestamp help? I > didn't look at the specific step that uses sleep to work around. The issue here is related to ctime, which is not modified by that helper. It also uses utime() which does not seem to have a way to modify ctime (which makes sense). Thanks, -Stolee
Derrick Stolee <derrickstolee@github.com> writes: >> Does "test-tool chmtime" to tweak the filesystem timestamp help? I >> didn't look at the specific step that uses sleep to work around. > > The issue here is related to ctime, which is not modified by that > helper. It also uses utime() which does not seem to have a way to > modify ctime (which makes sense). Ahh, silly me. You're right.
diff --git a/read-cache.c b/read-cache.c index 3e0e7d41837..4df97e185e9 100644 --- a/read-cache.c +++ b/read-cache.c @@ -134,7 +134,7 @@ static void replace_index_entry(struct index_state *istate, int nr, struct cache void rename_index_entry_at(struct index_state *istate, int nr, const char *new_name) { - struct cache_entry *old_entry = istate->cache[nr], *new_entry; + struct cache_entry *old_entry = istate->cache[nr], *new_entry, *refreshed; int namelen = strlen(new_name); new_entry = make_empty_cache_entry(istate, namelen); @@ -147,7 +147,20 @@ void rename_index_entry_at(struct index_state *istate, int nr, const char *new_n cache_tree_invalidate_path(istate, old_entry->name); untracked_cache_remove_from_index(istate, old_entry->name); remove_index_entry_at(istate, nr); - add_index_entry(istate, new_entry, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); + + /* + * Refresh the new index entry. Using 'refresh_cache_entry' ensures + * we only update stat info if the entry is otherwise up-to-date (i.e., + * the contents/mode haven't changed). This ensures that we reflect the + * 'ctime' of the rename in the index without (incorrectly) updating + * the cached stat info to reflect unstaged changes on disk. + */ + refreshed = refresh_cache_entry(istate, new_entry, CE_MATCH_REFRESH); + if (refreshed && refreshed != new_entry) { + add_index_entry(istate, refreshed, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); + discard_cache_entry(new_entry); + } else + add_index_entry(istate, new_entry, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE); } void fill_stat_data(struct stat_data *sd, struct stat *st) diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh index 963356ba5f9..a402908142d 100755 --- a/t/t7001-mv.sh +++ b/t/t7001-mv.sh @@ -4,6 +4,25 @@ test_description='git mv in subdirs' . ./test-lib.sh . "$TEST_DIRECTORY"/lib-diff-data.sh +test_expect_success 'mv -f refreshes updated index entry' ' + echo test >bar && + git add bar && + git commit -m test && + + echo foo >foo && + git add foo && + + # Wait one second to ensure ctime of rename will differ from original + # file creation ctime. + sleep 1 && + git mv -f foo bar && + git reset --merge HEAD && + + # Verify the index has been reset + git diff-files >out && + test_must_be_empty out +' + test_expect_success 'prepare reference tree' ' mkdir path0 path1 && COPYING_test_data >path0/COPYING &&