Skip to content
Open
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
19 changes: 19 additions & 0 deletions advice.c
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,22 @@ void detach_advice(const char *new_name)

fprintf(stderr, fmt, new_name);
}

void advise_on_moving_dirty_path(struct string_list *pathspec_list)
{
struct string_list_item *item;

if (!pathspec_list->nr)
return;

fprintf(stderr, _("The following paths have been moved outside the\n"
"sparse-checkout definition but are not sparse due to local\n"
"modifications.\n"));
for_each_string_list_item(item, pathspec_list)
fprintf(stderr, "%s\n", item->string);

advise_if_enabled(ADVICE_UPDATE_SPARSE_PATH,
_("To correct the sparsity of these paths, do the following:\n"
"* Use \"git add --sparse <paths>\" to update the index\n"
"* Use \"git sparse-checkout reapply\" to apply the sparsity rules"));
}
1 change: 1 addition & 0 deletions advice.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,6 @@ void NORETURN die_conclude_merge(void);
void NORETURN die_ff_impossible(void);
void advise_on_updating_sparse_paths(struct string_list *pathspec_list);
void detach_advice(const char *new_name);
void advise_on_moving_dirty_path(struct string_list *pathspec_list);

#endif /* ADVICE_H */
100 changes: 83 additions & 17 deletions builtin/mv.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ static const char * const builtin_mv_usage[] = {
};

enum update_mode {
BOTH = 0,
WORKING_DIRECTORY = (1 << 1),
INDEX = (1 << 2),
SPARSE = (1 << 3),
Expand Down Expand Up @@ -132,9 +131,15 @@ static int index_range_of_same_dir(const char *src, int length,
* Return 0 if such directory exist (i.e. with any of its contained files not
* marked with CE_SKIP_WORKTREE, the directory would be present in working tree).
* Return 1 otherwise.
*
* Note: *always* check the directory is not on-disk before this function
* (i.e. using lstat());
* otherwise it may return a false positive for a partially sparsified
* directory.
*/
static int check_dir_in_index(const char *name)
{
int ret = 1;
const char *with_slash = add_slash(name);
int length = strlen(with_slash);

Expand All @@ -144,14 +149,18 @@ static int check_dir_in_index(const char *name)
if (pos < 0) {
pos = -pos - 1;
if (pos >= the_index.cache_nr)
return 1;
goto free_return;
ce = active_cache[pos];
if (strncmp(with_slash, ce->name, length))
return 1;
goto free_return;
if (ce_skip_worktree(ce))
return 0;
ret = 0;
}
return 1;

free_return:
if (with_slash != name)
free((char *)with_slash);
return ret;
}

int cmd_mv(int argc, const char **argv, const char *prefix)
Expand All @@ -168,12 +177,14 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
OPT_END(),
};
const char **source, **destination, **dest_path, **submodule_gitfile;
enum update_mode *modes;
const char *dst_w_slash;
enum update_mode *modes, dst_mode = 0;
struct stat st;
struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
struct lock_file lock_file = LOCK_INIT;
struct cache_entry *ce;
struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP;
struct string_list dirty_paths = STRING_LIST_INIT_NODUP;

git_config(git_default_config, NULL);

Expand All @@ -198,20 +209,28 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
if (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1]))
flags = 0;
dest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags);
dst_w_slash = add_slash(dest_path[0]);
submodule_gitfile = xcalloc(argc, sizeof(char *));

if (dest_path[0][0] == '\0')
/* special case: "." was normalized to "" */
destination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME);
else if (!lstat(dest_path[0], &st) &&
S_ISDIR(st.st_mode)) {
dest_path[0] = add_slash(dest_path[0]);
destination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME);
destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);
} else {
if (argc != 1)
if (!path_in_sparse_checkout(dst_w_slash, &the_index) &&
!check_dir_in_index(dst_w_slash)) {
destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME);
dst_mode |= SKIP_WORKTREE_DIR;
} else if (argc != 1) {
die(_("destination '%s' is not a directory"), dest_path[0]);
destination = dest_path;
} else {
destination = dest_path;
}
}
if (dst_w_slash != dest_path[0])
free((char *)dst_w_slash);

/* Checking */
for (i = 0; i < argc; i++) {
Expand Down Expand Up @@ -346,6 +365,18 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
goto act_on_entry;
}

if (ignore_sparse &&
(dst_mode & SKIP_WORKTREE_DIR) &&
index_entry_exists(&the_index, dst, strlen(dst))) {
bad = _("destination exists in the index");
if (force) {
if (verbose)
warning(_("overwriting '%s'"), dst);
bad = NULL;
} else {
goto act_on_entry;
}
}
/*
* We check if the paths are in the sparse-checkout
* definition as a very final check, since that
Expand Down Expand Up @@ -396,6 +427,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
const char *src = source[i], *dst = destination[i];
enum update_mode mode = modes[i];
int pos;
int up_to_date = 0;
struct checkout state = CHECKOUT_INIT;
state.istate = &the_index;

Expand All @@ -406,6 +438,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
if (show_only)
continue;
if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
!(dst_mode & SKIP_WORKTREE_DIR) &&
rename(src, dst) < 0) {
if (ignore_errors)
continue;
Expand All @@ -425,20 +458,52 @@ int cmd_mv(int argc, const char **argv, const char *prefix)

pos = cache_name_pos(src, strlen(src));
assert(pos >= 0);
if (!(mode & SPARSE) && !lstat(src, &st))
up_to_date = !ce_modified(active_cache[pos], &st, 0);
rename_cache_entry_at(pos, dst);

if ((mode & SPARSE) &&
(path_in_sparse_checkout(dst, &the_index))) {
int dst_pos;
if (ignore_sparse &&
core_apply_sparse_checkout &&
core_sparse_checkout_cone) {

/* from out-of-cone to in-cone */
if ((mode & SPARSE) &&
path_in_sparse_checkout(dst, &the_index)) {
int dst_pos = cache_name_pos(dst, strlen(dst));
struct cache_entry *dst_ce = active_cache[dst_pos];

dst_pos = cache_name_pos(dst, strlen(dst));
active_cache[dst_pos]->ce_flags &= ~CE_SKIP_WORKTREE;
dst_ce->ce_flags &= ~CE_SKIP_WORKTREE;

if (checkout_entry(active_cache[dst_pos], &state, NULL, NULL))
die(_("cannot checkout %s"), active_cache[dst_pos]->name);
if (checkout_entry(dst_ce, &state, NULL, NULL))
die(_("cannot checkout %s"), dst_ce->name);
continue;
}

/* from in-cone to out-of-cone */
if ((dst_mode & SKIP_WORKTREE_DIR) &&
!(mode & SPARSE) &&
!path_in_sparse_checkout(dst, &the_index)) {
int dst_pos = cache_name_pos(dst, strlen(dst));
struct cache_entry *dst_ce = active_cache[dst_pos];
char *src_dir = dirname(xstrdup(src));

if (up_to_date) {
dst_ce->ce_flags |= CE_SKIP_WORKTREE;
unlink_or_warn(src);
} else {
string_list_append(&dirty_paths, dst);
safe_create_leading_directories(xstrdup(dst));
rename(src, dst);
}
if ((mode & INDEX) && is_empty_dir(src_dir))
rmdir_or_warn(src_dir);
}
}
}

if (dirty_paths.nr)
advise_on_moving_dirty_path(&dirty_paths);

if (gitmodules_modified)
stage_updated_gitmodules(&the_index);

Expand All @@ -447,6 +512,7 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
die(_("Unable to write new index file"));

string_list_clear(&src_for_dst, 0);
string_list_clear(&dirty_paths, 0);
UNLEAK(source);
UNLEAK(dest_path);
free(submodule_gitfile);
Expand Down
148 changes: 147 additions & 1 deletion t/t7002-mv-sparse-checkout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,25 @@ test_expect_success 'setup' "
updated in the index:
EOF

cat >sparse_hint <<-EOF
cat >sparse_hint <<-EOF &&
hint: If you intend to update such entries, try one of the following:
hint: * Use the --sparse option.
hint: * Disable or modify the sparsity rules.
hint: Disable this message with \"git config advice.updateSparsePath false\"
EOF

cat >dirty_error_header <<-EOF &&
The following paths have been moved outside the
sparse-checkout definition but are not sparse due to local
modifications.
EOF

cat >dirty_hint <<-EOF
hint: To correct the sparsity of these paths, do the following:
hint: * Use \"git add --sparse <paths>\" to update the index
hint: * Use \"git sparse-checkout reapply\" to apply the sparsity rules
hint: Disable this message with \"git config advice.updateSparsePath false\"
EOF
"

test_expect_success 'mv refuses to move sparse-to-sparse' '
Expand Down Expand Up @@ -290,4 +303,137 @@ test_expect_success 'move sparse file to existing destination with --force and -
test_cmp expect sub/file1
'

test_expect_success 'move clean path from in-cone to out-of-cone' '
test_when_finished "cleanup_sparse_checkout" &&
setup_sparse_checkout &&

test_must_fail git mv sub/d folder1 2>stderr &&
cat sparse_error_header >expect &&
echo "folder1/d" >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&

git mv --sparse sub/d folder1 2>stderr &&
test_must_be_empty stderr &&

test_path_is_missing sub/d &&
test_path_is_missing folder1/d &&
git ls-files -t >actual &&
! grep -x "H sub/d" actual &&
grep -x "S folder1/d" actual
'

test_expect_success 'move clean path from in-cone to out-of-cone overwrite' '
test_when_finished "cleanup_sparse_checkout" &&
setup_sparse_checkout &&
echo "sub/file1 overwrite" >sub/file1 &&
git add sub/file1 &&

test_must_fail git mv sub/file1 folder1 2>stderr &&
cat sparse_error_header >expect &&
echo "folder1/file1" >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&

test_must_fail git mv --sparse sub/file1 folder1 2>stderr &&
echo "fatal: destination exists in the index, source=sub/file1, destination=folder1/file1" \
>expect &&
test_cmp expect stderr &&

git mv --sparse -f sub/file1 folder1 2>stderr &&
test_must_be_empty stderr &&

test_path_is_missing sub/file1 &&
test_path_is_missing folder1/file1 &&
git ls-files -t >actual &&
! grep -x "H sub/file1" actual &&
grep -x "S folder1/file1" actual &&

# compare file content before move and after move
echo "sub/file1 overwrite" >expect &&
git ls-files -s -- folder1/file1 | awk "{print \$2}" >oid &&
git cat-file blob $(cat oid) >actual &&
test_cmp expect actual
'

test_expect_success 'move dirty path from in-cone to out-of-cone' '
test_when_finished "cleanup_sparse_checkout" &&
setup_sparse_checkout &&
echo "modified" >>sub/d &&

test_must_fail git mv sub/d folder1 2>stderr &&
cat sparse_error_header >expect &&
echo "folder1/d" >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&

git mv --sparse sub/d folder1 2>stderr &&
cat dirty_error_header >expect &&
echo "folder1/d" >>expect &&
cat dirty_hint >>expect &&
test_cmp expect stderr &&

test_path_is_missing sub/d &&
test_path_is_file folder1/d &&
git ls-files -t >actual &&
! grep -x "H sub/d" actual &&
grep -x "H folder1/d" actual
'

test_expect_success 'move dir from in-cone to out-of-cone' '
test_when_finished "cleanup_sparse_checkout" &&
setup_sparse_checkout &&

test_must_fail git mv sub/dir folder1 2>stderr &&
cat sparse_error_header >expect &&
echo "folder1/dir/e" >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&

git mv --sparse sub/dir folder1 2>stderr &&
test_must_be_empty stderr &&

test_path_is_missing sub/dir &&
test_path_is_missing folder1 &&
git ls-files -t >actual &&
! grep -x "H sub/dir/e" actual &&
grep -x "S folder1/dir/e" actual
'

test_expect_success 'move partially-dirty dir from in-cone to out-of-cone' '
test_when_finished "cleanup_sparse_checkout" &&
setup_sparse_checkout &&
touch sub/dir/e2 sub/dir/e3 &&
git add sub/dir/e2 sub/dir/e3 &&
echo "modified" >>sub/dir/e2 &&
echo "modified" >>sub/dir/e3 &&

test_must_fail git mv sub/dir folder1 2>stderr &&
cat sparse_error_header >expect &&
echo "folder1/dir/e" >>expect &&
echo "folder1/dir/e2" >>expect &&
echo "folder1/dir/e3" >>expect &&
cat sparse_hint >>expect &&
test_cmp expect stderr &&

git mv --sparse sub/dir folder1 2>stderr &&
cat dirty_error_header >expect &&
echo "folder1/dir/e2" >>expect &&
echo "folder1/dir/e3" >>expect &&
cat dirty_hint >>expect &&
test_cmp expect stderr &&

test_path_is_missing sub/dir &&
test_path_is_missing folder1/dir/e &&
test_path_is_file folder1/dir/e2 &&
test_path_is_file folder1/dir/e3 &&
git ls-files -t >actual &&
! grep -x "H sub/dir/e" actual &&
! grep -x "H sub/dir/e2" actual &&
! grep -x "H sub/dir/e3" actual &&
grep -x "S folder1/dir/e" actual &&
grep -x "H folder1/dir/e2" actual &&
grep -x "H folder1/dir/e3" actual
'

test_done