mirror of
https://git.proxmox.com/git/libgit2
synced 2025-08-07 11:46:18 +00:00
checkout: remove files before writing new ones
On case insensitive filesystems, we may have files in the working directory that case fold to a name we want to write. Remove those files (by default) so that we will not end up with a filename that has the unexpected case.
This commit is contained in:
parent
c2dee0fcb0
commit
e74340b000
@ -104,6 +104,11 @@ GIT_BEGIN_DECL
|
|||||||
* overwritten. Normally, files that are ignored in the working directory
|
* overwritten. Normally, files that are ignored in the working directory
|
||||||
* are not considered "precious" and may be overwritten if the checkout
|
* are not considered "precious" and may be overwritten if the checkout
|
||||||
* target contains that file.
|
* target contains that file.
|
||||||
|
*
|
||||||
|
* - GIT_CHECKOUT_DONT_REMOVE_EXISTING prevents checkout from removing
|
||||||
|
* files or folders that fold to the same name on case insensitive
|
||||||
|
* filesystems. This can cause files to retain their existing names
|
||||||
|
* and write through existing symbolic links.
|
||||||
*/
|
*/
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GIT_CHECKOUT_NONE = 0, /**< default is a dry run, no actual updates */
|
GIT_CHECKOUT_NONE = 0, /**< default is a dry run, no actual updates */
|
||||||
@ -158,6 +163,9 @@ typedef enum {
|
|||||||
/** Include common ancestor data in diff3 format files for conflicts */
|
/** Include common ancestor data in diff3 format files for conflicts */
|
||||||
GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 = (1u << 21),
|
GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 = (1u << 21),
|
||||||
|
|
||||||
|
/** Don't overwrite existing files or folders */
|
||||||
|
GIT_CHECKOUT_DONT_REMOVE_EXISTING = (1u << 22),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
|
* THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
|
||||||
*/
|
*/
|
||||||
|
@ -1310,10 +1310,27 @@ static int checkout_mkdir(
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool should_remove_existing(checkout_data *data)
|
||||||
|
{
|
||||||
|
int ignorecase = 0;
|
||||||
|
|
||||||
|
git_repository__cvar(&ignorecase, data->repo, GIT_CVAR_IGNORECASE);
|
||||||
|
|
||||||
|
return (ignorecase &&
|
||||||
|
(data->strategy & GIT_CHECKOUT_DONT_REMOVE_EXISTING) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MKDIR_NORMAL \
|
||||||
|
GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR
|
||||||
|
#define MKDIR_REMOVE_EXISTING \
|
||||||
|
MKDIR_NORMAL | GIT_MKDIR_REMOVE_FILES | GIT_MKDIR_REMOVE_SYMLINKS
|
||||||
|
|
||||||
static int mkpath2file(
|
static int mkpath2file(
|
||||||
checkout_data *data, const char *path, unsigned int mode)
|
checkout_data *data, const char *path, unsigned int mode)
|
||||||
{
|
{
|
||||||
git_buf *mkdir_path = &data->tmp;
|
git_buf *mkdir_path = &data->tmp;
|
||||||
|
struct stat st;
|
||||||
|
bool remove_existing = should_remove_existing(data);
|
||||||
int error;
|
int error;
|
||||||
|
|
||||||
if ((error = git_buf_sets(mkdir_path, path)) < 0)
|
if ((error = git_buf_sets(mkdir_path, path)) < 0)
|
||||||
@ -1321,14 +1338,36 @@ static int mkpath2file(
|
|||||||
|
|
||||||
git_buf_rtruncate_at_char(mkdir_path, '/');
|
git_buf_rtruncate_at_char(mkdir_path, '/');
|
||||||
|
|
||||||
if (data->last_mkdir.size && mkdir_path->size == data->last_mkdir.size &&
|
if (!data->last_mkdir.size ||
|
||||||
memcmp(mkdir_path->ptr, data->last_mkdir.ptr, mkdir_path->size) == 0)
|
data->last_mkdir.size != mkdir_path->size ||
|
||||||
return 0;
|
memcmp(mkdir_path->ptr, data->last_mkdir.ptr, mkdir_path->size) != 0) {
|
||||||
|
|
||||||
|
if ((error = checkout_mkdir(
|
||||||
|
data, mkdir_path->ptr, data->opts.target_directory, mode,
|
||||||
|
remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0)
|
||||||
|
return error;
|
||||||
|
|
||||||
if ((error = checkout_mkdir(
|
|
||||||
data, mkdir_path->ptr, data->opts.target_directory, mode,
|
|
||||||
GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR)) == 0)
|
|
||||||
git_buf_swap(&data->last_mkdir, mkdir_path);
|
git_buf_swap(&data->last_mkdir, mkdir_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove_existing) {
|
||||||
|
data->perfdata.stat_calls++;
|
||||||
|
|
||||||
|
if (p_lstat(path, &st) == 0) {
|
||||||
|
|
||||||
|
/* Some file, symlink or folder already exists at this name.
|
||||||
|
* We would have removed it in remove_the_old unless we're on
|
||||||
|
* a case inensitive filesystem (or the user has asked us not
|
||||||
|
* to). Remove the similarly named file to write the new.
|
||||||
|
*/
|
||||||
|
error = git_futils_rmdir_r(path, NULL, GIT_RMDIR_REMOVE_FILES);
|
||||||
|
} else if (errno != ENOENT) {
|
||||||
|
giterr_set(GITERR_OS, "Failed to stat file '%s'", path);
|
||||||
|
return GIT_EEXISTS;
|
||||||
|
} else {
|
||||||
|
giterr_clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
@ -1489,6 +1528,7 @@ static int checkout_submodule(
|
|||||||
checkout_data *data,
|
checkout_data *data,
|
||||||
const git_diff_file *file)
|
const git_diff_file *file)
|
||||||
{
|
{
|
||||||
|
bool remove_existing = should_remove_existing(data);
|
||||||
int error = 0;
|
int error = 0;
|
||||||
|
|
||||||
/* Until submodules are supported, UPDATE_ONLY means do nothing here */
|
/* Until submodules are supported, UPDATE_ONLY means do nothing here */
|
||||||
@ -1497,8 +1537,8 @@ static int checkout_submodule(
|
|||||||
|
|
||||||
if ((error = checkout_mkdir(
|
if ((error = checkout_mkdir(
|
||||||
data,
|
data,
|
||||||
file->path, data->opts.target_directory,
|
file->path, data->opts.target_directory, data->opts.dir_mode,
|
||||||
data->opts.dir_mode, GIT_MKDIR_PATH)) < 0)
|
remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0)
|
||||||
return error;
|
return error;
|
||||||
|
|
||||||
if ((error = git_submodule_lookup(NULL, data->repo, file->path)) < 0) {
|
if ((error = git_submodule_lookup(NULL, data->repo, file->path)) < 0) {
|
||||||
|
@ -279,6 +279,48 @@ void git_futils_mmap_free(git_map *out)
|
|||||||
p_munmap(out);
|
p_munmap(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GIT_INLINE(int) validate_existing(
|
||||||
|
const char *make_path,
|
||||||
|
struct stat *st,
|
||||||
|
mode_t mode,
|
||||||
|
uint32_t flags,
|
||||||
|
struct git_futils_mkdir_perfdata *perfdata)
|
||||||
|
{
|
||||||
|
if ((S_ISREG(st->st_mode) && (flags & GIT_MKDIR_REMOVE_FILES)) ||
|
||||||
|
(S_ISLNK(st->st_mode) && (flags & GIT_MKDIR_REMOVE_SYMLINKS))) {
|
||||||
|
if (p_unlink(make_path) < 0) {
|
||||||
|
giterr_set(GITERR_OS, "Failed to remove %s '%s'",
|
||||||
|
S_ISLNK(st->st_mode) ? "symlink" : "file", make_path);
|
||||||
|
return GIT_EEXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
perfdata->mkdir_calls++;
|
||||||
|
|
||||||
|
if (p_mkdir(make_path, mode) < 0) {
|
||||||
|
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path);
|
||||||
|
return GIT_EEXISTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (S_ISLNK(st->st_mode)) {
|
||||||
|
/* Re-stat the target, make sure it's a directory */
|
||||||
|
perfdata->stat_calls++;
|
||||||
|
|
||||||
|
if (p_stat(make_path, st) < 0) {
|
||||||
|
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path);
|
||||||
|
return GIT_EEXISTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (!S_ISDIR(st->st_mode)) {
|
||||||
|
giterr_set(GITERR_FILESYSTEM,
|
||||||
|
"Failed to make directory '%s': directory exists", make_path);
|
||||||
|
return GIT_EEXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int git_futils_mkdir_withperf(
|
int git_futils_mkdir_withperf(
|
||||||
const char *path,
|
const char *path,
|
||||||
const char *base,
|
const char *base,
|
||||||
@ -373,22 +415,9 @@ int git_futils_mkdir_withperf(
|
|||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (S_ISLNK(st.st_mode)) {
|
if ((error = validate_existing(
|
||||||
perfdata->stat_calls++;
|
make_path.ptr, &st, mode, flags, perfdata)) < 0)
|
||||||
|
|
||||||
/* Re-stat the target, make sure it's a directory */
|
|
||||||
if (p_stat(make_path.ptr, &st) < 0) {
|
|
||||||
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path.ptr);
|
|
||||||
error = GIT_EEXISTS;
|
|
||||||
goto done;
|
goto done;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!S_ISDIR(st.st_mode)) {
|
|
||||||
giterr_set(GITERR_FILESYSTEM, "Failed to make directory '%s': directory exists", make_path.ptr);
|
|
||||||
error = GIT_EEXISTS;
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* chmod if requested and necessary */
|
/* chmod if requested and necessary */
|
||||||
@ -400,7 +429,8 @@ int git_futils_mkdir_withperf(
|
|||||||
|
|
||||||
if ((error = p_chmod(make_path.ptr, mode)) < 0 &&
|
if ((error = p_chmod(make_path.ptr, mode)) < 0 &&
|
||||||
lastch == '\0') {
|
lastch == '\0') {
|
||||||
giterr_set(GITERR_OS, "Failed to set permissions on '%s'", make_path.ptr);
|
giterr_set(GITERR_OS, "Failed to set permissions on '%s'",
|
||||||
|
make_path.ptr);
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +444,8 @@ int git_futils_mkdir_withperf(
|
|||||||
perfdata->stat_calls++;
|
perfdata->stat_calls++;
|
||||||
|
|
||||||
if (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode)) {
|
if (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode)) {
|
||||||
giterr_set(GITERR_OS, "Path is not a directory '%s'", make_path.ptr);
|
giterr_set(GITERR_OS, "Path is not a directory '%s'",
|
||||||
|
make_path.ptr);
|
||||||
error = GIT_ENOTFOUND;
|
error = GIT_ENOTFOUND;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,8 @@ extern int git_futils_mkdir_r(const char *path, const char *base, const mode_t m
|
|||||||
* * GIT_MKDIR_SKIP_LAST says to leave off the last element of the path
|
* * GIT_MKDIR_SKIP_LAST says to leave off the last element of the path
|
||||||
* * GIT_MKDIR_SKIP_LAST2 says to leave off the last 2 elements of the path
|
* * GIT_MKDIR_SKIP_LAST2 says to leave off the last 2 elements of the path
|
||||||
* * GIT_MKDIR_VERIFY_DIR says confirm final item is a dir, not just EEXIST
|
* * GIT_MKDIR_VERIFY_DIR says confirm final item is a dir, not just EEXIST
|
||||||
|
* * GIT_MKDIR_REMOVE_FILES says to remove files and recreate dirs
|
||||||
|
* * GIT_MKDIR_REMOVE_SYMLINKS says to remove symlinks and recreate dirs
|
||||||
*
|
*
|
||||||
* Note that the chmod options will be executed even if the directory already
|
* Note that the chmod options will be executed even if the directory already
|
||||||
* exists, unless GIT_MKDIR_EXCL is given.
|
* exists, unless GIT_MKDIR_EXCL is given.
|
||||||
@ -82,6 +84,8 @@ typedef enum {
|
|||||||
GIT_MKDIR_SKIP_LAST = 16,
|
GIT_MKDIR_SKIP_LAST = 16,
|
||||||
GIT_MKDIR_SKIP_LAST2 = 32,
|
GIT_MKDIR_SKIP_LAST2 = 32,
|
||||||
GIT_MKDIR_VERIFY_DIR = 64,
|
GIT_MKDIR_VERIFY_DIR = 64,
|
||||||
|
GIT_MKDIR_REMOVE_FILES = 128,
|
||||||
|
GIT_MKDIR_REMOVE_SYMLINKS = 256,
|
||||||
} git_futils_mkdir_flags;
|
} git_futils_mkdir_flags;
|
||||||
|
|
||||||
struct git_futils_mkdir_perfdata
|
struct git_futils_mkdir_perfdata
|
||||||
|
97
tests/checkout/icase.c
Normal file
97
tests/checkout/icase.c
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
#include "clar_libgit2.h"
|
||||||
|
|
||||||
|
#include "git2/checkout.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
static git_repository *repo;
|
||||||
|
static git_object *obj;
|
||||||
|
static git_checkout_options checkout_opts;
|
||||||
|
|
||||||
|
void test_checkout_icase__initialize(void)
|
||||||
|
{
|
||||||
|
git_oid id;
|
||||||
|
|
||||||
|
repo = cl_git_sandbox_init("testrepo");
|
||||||
|
|
||||||
|
cl_git_pass(git_reference_name_to_id(&id, repo, "refs/heads/dir"));
|
||||||
|
cl_git_pass(git_object_lookup(&obj, repo, &id, GIT_OBJ_ANY));
|
||||||
|
|
||||||
|
git_checkout_init_options(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION);
|
||||||
|
checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__cleanup(void)
|
||||||
|
{
|
||||||
|
git_object_free(obj);
|
||||||
|
cl_git_sandbox_cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assert_name_is(const char *expected)
|
||||||
|
{
|
||||||
|
char *actual;
|
||||||
|
size_t actual_len, expected_len, start;
|
||||||
|
|
||||||
|
cl_assert(actual = realpath(expected, NULL));
|
||||||
|
|
||||||
|
expected_len = strlen(expected);
|
||||||
|
actual_len = strlen(actual);
|
||||||
|
cl_assert(actual_len >= expected_len);
|
||||||
|
|
||||||
|
start = actual_len - expected_len;
|
||||||
|
cl_assert_equal_s(expected, actual + start);
|
||||||
|
|
||||||
|
if (start)
|
||||||
|
cl_assert_equal_strn("/", actual + (start - 1), 1);
|
||||||
|
|
||||||
|
free(actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__overwrites_files_for_files(void)
|
||||||
|
{
|
||||||
|
cl_git_write2file("testrepo/NEW.txt", "neue file\n", 10, \
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
|
||||||
|
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
|
||||||
|
assert_name_is("testrepo/new.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__overwrites_links_for_files(void)
|
||||||
|
{
|
||||||
|
cl_must_pass(p_symlink("../tmp", "testrepo/NEW.txt"));
|
||||||
|
|
||||||
|
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
|
||||||
|
|
||||||
|
cl_assert(!git_path_exists("tmp"));
|
||||||
|
assert_name_is("testrepo/new.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__overwites_folders_for_files(void)
|
||||||
|
{
|
||||||
|
cl_must_pass(p_mkdir("testrepo/NEW.txt", 0777));
|
||||||
|
|
||||||
|
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
|
||||||
|
|
||||||
|
assert_name_is("testrepo/new.txt");
|
||||||
|
cl_assert(!git_path_isdir("testrepo/new.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__overwrites_files_for_folders(void)
|
||||||
|
{
|
||||||
|
cl_git_write2file("testrepo/A", "neue file\n", 10, \
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
|
||||||
|
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
|
||||||
|
assert_name_is("testrepo/a");
|
||||||
|
cl_assert(git_path_isdir("testrepo/a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_checkout_icase__overwrites_links_for_folders(void)
|
||||||
|
{
|
||||||
|
cl_must_pass(p_symlink("..", "testrepo/A"));
|
||||||
|
|
||||||
|
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
|
||||||
|
|
||||||
|
cl_assert(!git_path_exists("b.txt"));
|
||||||
|
assert_name_is("testrepo/a");
|
||||||
|
}
|
||||||
|
|
@ -279,10 +279,10 @@ void test_checkout_index__options_open_flags(void)
|
|||||||
|
|
||||||
cl_git_mkfile("./testrepo/new.txt", "hi\n");
|
cl_git_mkfile("./testrepo/new.txt", "hi\n");
|
||||||
|
|
||||||
opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
|
opts.checkout_strategy =
|
||||||
|
GIT_CHECKOUT_FORCE | GIT_CHECKOUT_DONT_REMOVE_EXISTING;
|
||||||
opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND;
|
opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND;
|
||||||
|
|
||||||
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
|
|
||||||
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
|
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
|
||||||
|
|
||||||
check_file_contents("./testrepo/new.txt", "hi\nmy new file\n");
|
check_file_contents("./testrepo/new.txt", "hi\nmy new file\n");
|
||||||
|
Loading…
Reference in New Issue
Block a user