diff --git a/builtin/merge.c b/builtin/merge.c index 02a87430f48f97..d023c4d691c765 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -371,6 +371,9 @@ static int save_state(struct object_id *stash) die(_("not a valid object: %s"), buffer.buf); rc = 0; out: + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) + die(_("could not read index")); strbuf_release(&buffer); return rc; } diff --git a/builtin/stash.c b/builtin/stash.c index 170024c4940b59..7bb0b446844b62 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -33,6 +33,7 @@ #include "add-interactive.h" #include "oid-array.h" #include "commit.h" +#include "virtualfilesystem.h" #define INCLUDE_ALL_FILES 2 @@ -1285,9 +1286,10 @@ static int check_changes_tracked_files(const struct pathspec *ps) static int check_changes(const struct pathspec *ps, int include_untracked, struct strbuf *untracked_files) { - int ret = 0; - if (check_changes_tracked_files(ps)) - ret = 1; + int ret = check_changes_tracked_files(ps); + + if (ret < 0) + return ret; if (include_untracked && get_untracked_files(ps, include_untracked, untracked_files)) @@ -1296,6 +1298,24 @@ static int check_changes(const struct pathspec *ps, int include_untracked, return ret; } +static int check_tracked_files_accessible(const struct pathspec *ps) +{ + struct index_state *istate = the_repository->index; + struct stat st; + + for (size_t i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + + if (ce_stage(ce) || ce_skip_worktree(ce) || + !ce_path_match(istate, ce, ps, NULL)) + continue; + if (lstat(ce->name, &st) < 0 && + errno != ENOENT && errno != ENOTDIR) + return error_errno(_("cannot stat '%s'"), ce->name); + } + return 0; +} + static int save_untracked_files(struct stash_info *info, struct strbuf *msg, struct strbuf *files) { @@ -1521,9 +1541,30 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b prepare_fallback_ident("git stash", "git@stash"); repo_read_index_preload(the_repository, NULL, 0); - if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, - NULL, NULL, NULL) < 0) { - ret = error(_("could not write index")); + ret = repo_refresh_and_write_index(the_repository, + REFRESH_QUIET | + REFRESH_FAIL_ON_STAT_ERROR, + 0, 0, NULL, NULL, NULL); + if (ret) { + ret = error(ret < 0 ? + _("could not write index") : + _("could not refresh index")); + goto done; + } + + free_virtualfilesystem(); + enable_fscache(0); + flush_fscache(); + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) { + ret = error(_("could not read index")); + goto done; + } + for (size_t i = 0; i < the_repository->index->cache_nr; i++) + the_repository->index->cache[i]->ce_flags &= + ~(CE_UPTODATE | CE_FSMONITOR_VALID); + if (check_tracked_files_accessible(ps) < 0) { + ret = -1; goto done; } @@ -1542,10 +1583,8 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b } } - if (!check_changes(ps, include_untracked, &untracked_files)) { - ret = 1; - goto done; - } + if (include_untracked) + get_untracked_files(ps, include_untracked, &untracked_files); branch_ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), "HEAD", 0, NULL, &flags); @@ -1731,11 +1770,15 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q goto done; } - if (!check_changes(ps, include_untracked, &untracked_files)) { + ret = check_changes(ps, include_untracked, &untracked_files); + if (ret < 0) + goto done; + if (!ret) { if (!quiet) printf_ln(_("No local changes to save")); goto done; } + ret = 0; if (!refs_reflog_exists(get_main_ref_store(the_repository), ref_stash) && do_clear_stash()) { ret = -1; diff --git a/read-cache-ll.h b/read-cache-ll.h index 31fa370e43d379..851276d99c3e24 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -457,6 +457,8 @@ int fake_lstat(const struct cache_entry *ce, struct stat *st); #define REFRESH_IN_PORCELAIN (1 << 5) /* user friendly output, not "needs update" */ #define REFRESH_PROGRESS (1 << 6) /* show progress bar if stderr is tty */ #define REFRESH_IGNORE_SKIP_WORKTREE (1 << 7) /* ignore skip_worktree entries */ +/* Report unexpected stat errors even when REFRESH_QUIET is set. */ +#define REFRESH_FAIL_ON_STAT_ERROR (1 << 8) int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg); /* * Refresh the index and write it to disk. diff --git a/read-cache.c b/read-cache.c index 7653d13fa36e6f..360a8806a80648 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1508,6 +1508,7 @@ int refresh_index(struct index_state *istate, unsigned int flags, int not_new = (flags & REFRESH_IGNORE_MISSING) != 0; int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0; int ignore_skip_worktree = (flags & REFRESH_IGNORE_SKIP_WORKTREE) != 0; + int fail_on_stat_error = (flags & REFRESH_FAIL_ON_STAT_ERROR) != 0; int first = 1; int in_porcelain = (flags & REFRESH_IN_PORCELAIN); unsigned int options = (CE_MATCH_REFRESH | @@ -1603,8 +1604,13 @@ int refresh_index(struct index_state *istate, unsigned int flags, mark_fsmonitor_invalid(istate, ce); istate->cache_changed |= CE_ENTRY_CHANGED; } - if (quiet) + if (quiet) { + if (fail_on_stat_error && + cache_errno != EINVAL && + cache_errno != ENOENT) + has_errors = 1; continue; + } if (cache_errno == ENOENT) fmt = deleted_fmt; diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 4b306010053bcd..0a02f1ac1fd34c 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -640,6 +640,82 @@ test_expect_success 'stash create - no changes' ' test_must_be_empty actual ' +test_expect_success 'stash create - clean after index refresh' ' + test_create_repo create-refresh-race && + ( + cd create-refresh-race && + test_commit A file && + write_script .git/hooks/post-index-change <<-\EOF && + if test ! -e .git/hook-fired + then + >.git/hook-fired + test-tool chmtime -3600 file + fi + EOF + + # The first check sees a stat change. The hook changes only + # the mtime after refresh, so the stash tree remains clean. + test-tool chmtime +3600 file && + git stash create >actual && + test_file_not_empty actual && + test_cmp_rev "$(cat actual)^{tree}" HEAD^{tree} && + test_path_is_file .git/hook-fired + ) +' + +test_expect_success 'stash create - captures worktree changes during refresh' ' + test_create_repo create-content-race && + ( + cd create-content-race && + test_commit A file && + write_script .git/hooks/post-index-change <<-\EOF && + if test ! -e .git/hook-fired + then + >.git/hook-fired + echo changed >file + fi + EOF + + # A content change after refresh must be included in the stash. + test-tool chmtime +3600 file && + git stash create >actual && + git show "$(cat actual):file" >stashed && + test_cmp file stashed && + git diff --exit-code --cached && + test "changed" = "$(cat file)" + ) +' + +test_expect_success 'stash create - reloads virtual filesystem paths' ' + test_create_repo create-vfs-race && + ( + cd create-vfs-race && + echo visible >visible && + echo original >hidden && + git add visible hidden && + git commit -m initial && + printf "visible\0" >.git/vfs-paths && + write_script .git/hooks/virtualfilesystem <<-\EOF && + cat .git/vfs-paths + EOF + git config core.virtualfilesystem \ + .git/hooks/virtualfilesystem && + write_script .git/hooks/post-index-change <<-\EOF && + if test ! -e .git/hook-fired + then + >.git/hook-fired + echo changed >hidden + printf "visible\0hidden\0" >.git/vfs-paths + fi + EOF + + test-tool chmtime +3600 visible && + git stash create >actual && + git show "$(cat actual):hidden" >stashed && + test_cmp hidden stashed + ) +' + test_expect_success 'stash branch - no stashes on stack, stash-like argument' ' git stash clear && test_when_finished "git reset --hard HEAD" && diff --git a/t/t7600-merge.sh b/t/t7600-merge.sh index 64fe21717db0d4..aa76a3a27dadd9 100755 --- a/t/t7600-merge.sh +++ b/t/t7600-merge.sh @@ -1179,4 +1179,83 @@ test_expect_success 'merge with no argument defaults to upstream' ' ) ' +test_expect_success 'merge accepts a stash created after index refresh' ' + test_create_repo merge-refresh && + ( + cd merge-refresh && + echo unchanged >victim && + echo base >base && + git add base victim && + git commit -m base && + git branch -M main && + + git checkout -b side && + echo side >side && + git add side && + git commit -m side && + + git checkout main && + echo main >main && + git add main && + git commit -m main && + + write_script .git/hooks/post-index-change <<-\EOF && + if test ! -e .git/hook-fired + then + >.git/hook-fired + test-tool chmtime -3600 victim + fi + EOF + + test-tool chmtime +3600 victim && + git merge side && + test_path_is_file side && + test_path_is_file .git/hook-fired + ) +' + +test_expect_success 'merge autostash creates a stash after refresh' ' + test_create_repo merge-autostash-refresh && + ( + cd merge-autostash-refresh && + echo original >victim && + echo trigger >trigger && + echo base >base && + git add base trigger victim && + git commit -m base && + git branch -M main && + + git checkout -b side && + echo side >side && + git add side && + git commit -m side && + + git checkout main && + echo main >main && + git add main && + git commit -m main && + cp victim .git/victim-clean && + echo changed >victim && + + write_script .git/hooks/post-index-change <<-\EOF && + count=$(cat .git/hook-count 2>/dev/null || echo 0) + count=$((count + 1)) + echo $count >.git/hook-count + if test $count -eq 1 + then + test-tool chmtime -3600 trigger + elif test $count -eq 2 + then + cp .git/victim-clean victim + fi + EOF + + test-tool chmtime +3600 trigger && + git merge --autostash side && + test_path_is_file side && + test_cmp .git/victim-clean victim && + test "$(cat .git/hook-count)" -ge 2 + ) +' + test_done