Skip to content

Commit

Permalink
checkout: remove files before writing new ones
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ethomson committed Jan 16, 2015
1 parent 186177a commit 7268a5a
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 27 deletions.
8 changes: 8 additions & 0 deletions include/git2/checkout.h
Expand Up @@ -104,6 +104,11 @@ GIT_BEGIN_DECL
* overwritten. Normally, files that are ignored in the working directory
* are not considered "precious" and may be overwritten if the checkout
* 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 {
GIT_CHECKOUT_NONE = 0, /**< default is a dry run, no actual updates */
Expand Down Expand Up @@ -158,6 +163,9 @@ typedef enum {
/** Include common ancestor data in diff3 format files for conflicts */
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
*/
Expand Down
56 changes: 48 additions & 8 deletions src/checkout.c
Expand Up @@ -1214,25 +1214,64 @@ static int checkout_mkdir(
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(
checkout_data *data, const char *path, unsigned int mode)
{
git_buf *mkdir_path = &data->tmp;
struct stat st;
bool remove_existing = should_remove_existing(data);
int error;

if ((error = git_buf_sets(mkdir_path, path)) < 0)
return error;

git_buf_rtruncate_at_char(mkdir_path, '/');

if (data->last_mkdir.size && mkdir_path->size == data->last_mkdir.size &&
memcmp(mkdir_path->ptr, data->last_mkdir.ptr, mkdir_path->size) == 0)
return 0;
if (!data->last_mkdir.size ||
data->last_mkdir.size != mkdir_path->size ||
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);
}

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;
}
Expand Down Expand Up @@ -1393,6 +1432,7 @@ static int checkout_submodule(
checkout_data *data,
const git_diff_file *file)
{
bool remove_existing = should_remove_existing(data);
int error = 0;

/* Until submodules are supported, UPDATE_ONLY means do nothing here */
Expand All @@ -1401,8 +1441,8 @@ static int checkout_submodule(

if ((error = checkout_mkdir(
data,
file->path, data->opts.target_directory,
data->opts.dir_mode, GIT_MKDIR_PATH)) < 0)
file->path, data->opts.target_directory, data->opts.dir_mode,
remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0)
return error;

if ((error = git_submodule_lookup(NULL, data->repo, file->path)) < 0) {
Expand Down
65 changes: 48 additions & 17 deletions src/fileops.c
Expand Up @@ -279,6 +279,48 @@ void git_futils_mmap_free(git_map *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_INVALID,
"Failed to make directory '%s': directory exists", make_path);
return GIT_EEXISTS;
}

return 0;
}

int git_futils_mkdir_withperf(
const char *path,
const char *base,
Expand Down Expand Up @@ -373,22 +415,9 @@ int git_futils_mkdir_withperf(
goto done;
}

if (S_ISLNK(st.st_mode)) {
perfdata->stat_calls++;

/* 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;
if ((error = validate_existing(
make_path.ptr, &st, mode, flags, perfdata)) < 0)
goto done;
}
}

if (!S_ISDIR(st.st_mode)) {
giterr_set(GITERR_INVALID, "Failed to make directory '%s': directory exists", make_path.ptr);
error = GIT_EEXISTS;
goto done;
}
}

/* chmod if requested and necessary */
Expand All @@ -400,7 +429,8 @@ int git_futils_mkdir_withperf(

if ((error = p_chmod(make_path.ptr, mode)) < 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;
}
}
Expand All @@ -414,7 +444,8 @@ int git_futils_mkdir_withperf(
perfdata->stat_calls++;

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;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/fileops.h
Expand Up @@ -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_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_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
* exists, unless GIT_MKDIR_EXCL is given.
Expand All @@ -82,6 +84,8 @@ typedef enum {
GIT_MKDIR_SKIP_LAST = 16,
GIT_MKDIR_SKIP_LAST2 = 32,
GIT_MKDIR_VERIFY_DIR = 64,
GIT_MKDIR_REMOVE_FILES = 128,
GIT_MKDIR_REMOVE_SYMLINKS = 256,
} git_futils_mkdir_flags;

struct git_futils_mkdir_perfdata
Expand Down
97 changes: 97 additions & 0 deletions tests/checkout/icase.c
@@ -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");
}

4 changes: 2 additions & 2 deletions tests/checkout/index.c
Expand Up @@ -279,10 +279,10 @@ void test_checkout_index__options_open_flags(void)

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.checkout_strategy = GIT_CHECKOUT_FORCE;
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));

check_file_contents("./testrepo/new.txt", "hi\nmy new file\n");
Expand Down

0 comments on commit 7268a5a

Please sign in to comment.