Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions builtin/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
65 changes: 54 additions & 11 deletions builtin/stash.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "add-interactive.h"
#include "oid-array.h"
#include "commit.h"
#include "virtualfilesystem.h"

#define INCLUDE_ALL_FILES 2

Expand Down Expand Up @@ -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))
Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions read-cache-ll.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion read-cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions t/t3903-stash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" &&
Expand Down
79 changes: 79 additions & 0 deletions t/t7600-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading