Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 90bc025

Browse files
ethomsonEdward Thomson
authored and
Edward Thomson
committed
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.
1 parent 2889b72 commit 90bc025

File tree

6 files changed

+207
-27
lines changed

6 files changed

+207
-27
lines changed

include/git2/checkout.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ GIT_BEGIN_DECL
104104
* overwritten. Normally, files that are ignored in the working directory
105105
* are not considered "precious" and may be overwritten if the checkout
106106
* target contains that file.
107+
*
108+
* - GIT_CHECKOUT_DONT_REMOVE_EXISTING prevents checkout from removing
109+
* files or folders that fold to the same name on case insensitive
110+
* filesystems. This can cause files to retain their existing names
111+
* and write through existing symbolic links.
107112
*/
108113
typedef enum {
109114
GIT_CHECKOUT_NONE = 0, /**< default is a dry run, no actual updates */
@@ -158,6 +163,9 @@ typedef enum {
158163
/** Include common ancestor data in diff3 format files for conflicts */
159164
GIT_CHECKOUT_CONFLICT_STYLE_DIFF3 = (1u << 21),
160165

166+
/** Don't overwrite existing files or folders */
167+
GIT_CHECKOUT_DONT_REMOVE_EXISTING = (1u << 22),
168+
161169
/**
162170
* THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
163171
*/

src/checkout.c

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,25 +1239,64 @@ static int checkout_mkdir(
12391239
return error;
12401240
}
12411241

1242+
static bool should_remove_existing(checkout_data *data)
1243+
{
1244+
int ignorecase = 0;
1245+
1246+
git_repository__cvar(&ignorecase, data->repo, GIT_CVAR_IGNORECASE);
1247+
1248+
return (ignorecase &&
1249+
(data->strategy & GIT_CHECKOUT_DONT_REMOVE_EXISTING) == 0);
1250+
}
1251+
1252+
#define MKDIR_NORMAL \
1253+
GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR
1254+
#define MKDIR_REMOVE_EXISTING \
1255+
MKDIR_NORMAL | GIT_MKDIR_REMOVE_FILES | GIT_MKDIR_REMOVE_SYMLINKS
1256+
12421257
static int mkpath2file(
12431258
checkout_data *data, const char *path, unsigned int mode)
12441259
{
12451260
git_buf *mkdir_path = &data->tmp;
1261+
struct stat st;
1262+
bool remove_existing = should_remove_existing(data);
12461263
int error;
12471264

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

12511268
git_buf_rtruncate_at_char(mkdir_path, '/');
12521269

1253-
if (data->last_mkdir.size && mkdir_path->size == data->last_mkdir.size &&
1254-
memcmp(mkdir_path->ptr, data->last_mkdir.ptr, mkdir_path->size) == 0)
1255-
return 0;
1270+
if (!data->last_mkdir.size ||
1271+
data->last_mkdir.size != mkdir_path->size ||
1272+
memcmp(mkdir_path->ptr, data->last_mkdir.ptr, mkdir_path->size) != 0) {
1273+
1274+
if ((error = checkout_mkdir(
1275+
data, mkdir_path->ptr, data->opts.target_directory, mode,
1276+
remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0)
1277+
return error;
12561278

1257-
if ((error = checkout_mkdir(
1258-
data, mkdir_path->ptr, data->opts.target_directory, mode,
1259-
GIT_MKDIR_PATH | GIT_MKDIR_VERIFY_DIR)) == 0)
12601279
git_buf_swap(&data->last_mkdir, mkdir_path);
1280+
}
1281+
1282+
if (remove_existing) {
1283+
data->perfdata.stat_calls++;
1284+
1285+
if (p_lstat(path, &st) == 0) {
1286+
1287+
/* Some file, symlink or folder already exists at this name.
1288+
* We would have removed it in remove_the_old unless we're on
1289+
* a case inensitive filesystem (or the user has asked us not
1290+
* to). Remove the similarly named file to write the new.
1291+
*/
1292+
error = git_futils_rmdir_r(path, NULL, GIT_RMDIR_REMOVE_FILES);
1293+
} else if (errno != ENOENT) {
1294+
giterr_set(GITERR_OS, "Failed to stat file '%s'", path);
1295+
return GIT_EEXISTS;
1296+
} else {
1297+
giterr_clear();
1298+
}
1299+
}
12611300

12621301
return error;
12631302
}
@@ -1418,6 +1457,7 @@ static int checkout_submodule(
14181457
checkout_data *data,
14191458
const git_diff_file *file)
14201459
{
1460+
bool remove_existing = should_remove_existing(data);
14211461
int error = 0;
14221462

14231463
/* Until submodules are supported, UPDATE_ONLY means do nothing here */
@@ -1426,8 +1466,8 @@ static int checkout_submodule(
14261466

14271467
if ((error = checkout_mkdir(
14281468
data,
1429-
file->path, data->opts.target_directory,
1430-
data->opts.dir_mode, GIT_MKDIR_PATH)) < 0)
1469+
file->path, data->opts.target_directory, data->opts.dir_mode,
1470+
remove_existing ? MKDIR_REMOVE_EXISTING : MKDIR_NORMAL)) < 0)
14311471
return error;
14321472

14331473
if ((error = git_submodule_lookup(NULL, data->repo, file->path)) < 0) {

src/fileops.c

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,48 @@ void git_futils_mmap_free(git_map *out)
279279
p_munmap(out);
280280
}
281281

282+
GIT_INLINE(int) validate_existing(
283+
const char *make_path,
284+
struct stat *st,
285+
mode_t mode,
286+
uint32_t flags,
287+
struct git_futils_mkdir_perfdata *perfdata)
288+
{
289+
if ((S_ISREG(st->st_mode) && (flags & GIT_MKDIR_REMOVE_FILES)) ||
290+
(S_ISLNK(st->st_mode) && (flags & GIT_MKDIR_REMOVE_SYMLINKS))) {
291+
if (p_unlink(make_path) < 0) {
292+
giterr_set(GITERR_OS, "Failed to remove %s '%s'",
293+
S_ISLNK(st->st_mode) ? "symlink" : "file", make_path);
294+
return GIT_EEXISTS;
295+
}
296+
297+
perfdata->mkdir_calls++;
298+
299+
if (p_mkdir(make_path, mode) < 0) {
300+
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path);
301+
return GIT_EEXISTS;
302+
}
303+
}
304+
305+
else if (S_ISLNK(st->st_mode)) {
306+
/* Re-stat the target, make sure it's a directory */
307+
perfdata->stat_calls++;
308+
309+
if (p_stat(make_path, st) < 0) {
310+
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path);
311+
return GIT_EEXISTS;
312+
}
313+
}
314+
315+
else if (!S_ISDIR(st->st_mode)) {
316+
giterr_set(GITERR_INVALID,
317+
"Failed to make directory '%s': directory exists", make_path);
318+
return GIT_EEXISTS;
319+
}
320+
321+
return 0;
322+
}
323+
282324
int git_futils_mkdir_withperf(
283325
const char *path,
284326
const char *base,
@@ -373,22 +415,9 @@ int git_futils_mkdir_withperf(
373415
goto done;
374416
}
375417

376-
if (S_ISLNK(st.st_mode)) {
377-
perfdata->stat_calls++;
378-
379-
/* Re-stat the target, make sure it's a directory */
380-
if (p_stat(make_path.ptr, &st) < 0) {
381-
giterr_set(GITERR_OS, "Failed to make directory '%s'", make_path.ptr);
382-
error = GIT_EEXISTS;
418+
if ((error = validate_existing(
419+
make_path.ptr, &st, mode, flags, perfdata)) < 0)
383420
goto done;
384-
}
385-
}
386-
387-
if (!S_ISDIR(st.st_mode)) {
388-
giterr_set(GITERR_INVALID, "Failed to make directory '%s': directory exists", make_path.ptr);
389-
error = GIT_EEXISTS;
390-
goto done;
391-
}
392421
}
393422

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

401430
if ((error = p_chmod(make_path.ptr, mode)) < 0 &&
402431
lastch == '\0') {
403-
giterr_set(GITERR_OS, "Failed to set permissions on '%s'", make_path.ptr);
432+
giterr_set(GITERR_OS, "Failed to set permissions on '%s'",
433+
make_path.ptr);
404434
goto done;
405435
}
406436
}
@@ -414,7 +444,8 @@ int git_futils_mkdir_withperf(
414444
perfdata->stat_calls++;
415445

416446
if (p_stat(make_path.ptr, &st) < 0 || !S_ISDIR(st.st_mode)) {
417-
giterr_set(GITERR_OS, "Path is not a directory '%s'", make_path.ptr);
447+
giterr_set(GITERR_OS, "Path is not a directory '%s'",
448+
make_path.ptr);
418449
error = GIT_ENOTFOUND;
419450
}
420451
}

src/fileops.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ extern int git_futils_mkdir_r(const char *path, const char *base, const mode_t m
7070
* * GIT_MKDIR_SKIP_LAST says to leave off the last element of the path
7171
* * GIT_MKDIR_SKIP_LAST2 says to leave off the last 2 elements of the path
7272
* * GIT_MKDIR_VERIFY_DIR says confirm final item is a dir, not just EEXIST
73+
* * GIT_MKDIR_REMOVE_FILES says to remove files and recreate dirs
74+
* * GIT_MKDIR_REMOVE_SYMLINKS says to remove symlinks and recreate dirs
7375
*
7476
* Note that the chmod options will be executed even if the directory already
7577
* exists, unless GIT_MKDIR_EXCL is given.
@@ -82,6 +84,8 @@ typedef enum {
8284
GIT_MKDIR_SKIP_LAST = 16,
8385
GIT_MKDIR_SKIP_LAST2 = 32,
8486
GIT_MKDIR_VERIFY_DIR = 64,
87+
GIT_MKDIR_REMOVE_FILES = 128,
88+
GIT_MKDIR_REMOVE_SYMLINKS = 256,
8589
} git_futils_mkdir_flags;
8690

8791
struct git_futils_mkdir_perfdata

tests/checkout/icase.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "clar_libgit2.h"
2+
3+
#include "git2/checkout.h"
4+
#include "path.h"
5+
6+
static git_repository *repo;
7+
static git_object *obj;
8+
static git_checkout_options checkout_opts;
9+
10+
void test_checkout_icase__initialize(void)
11+
{
12+
git_oid id;
13+
14+
repo = cl_git_sandbox_init("testrepo");
15+
16+
cl_git_pass(git_reference_name_to_id(&id, repo, "refs/heads/dir"));
17+
cl_git_pass(git_object_lookup(&obj, repo, &id, GIT_OBJ_ANY));
18+
19+
git_checkout_init_options(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION);
20+
checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
21+
}
22+
23+
void test_checkout_icase__cleanup(void)
24+
{
25+
git_object_free(obj);
26+
cl_git_sandbox_cleanup();
27+
}
28+
29+
static void assert_name_is(const char *expected)
30+
{
31+
char *actual;
32+
size_t actual_len, expected_len, start;
33+
34+
cl_assert(actual = realpath(expected, NULL));
35+
36+
expected_len = strlen(expected);
37+
actual_len = strlen(actual);
38+
cl_assert(actual_len >= expected_len);
39+
40+
start = actual_len - expected_len;
41+
cl_assert_equal_s(expected, actual + start);
42+
43+
if (start)
44+
cl_assert_equal_strn("/", actual + (start - 1), 1);
45+
46+
free(actual);
47+
}
48+
49+
void test_checkout_icase__overwrites_files_for_files(void)
50+
{
51+
cl_git_write2file("testrepo/NEW.txt", "neue file\n", 10, \
52+
O_WRONLY | O_CREAT | O_TRUNC, 0644);
53+
54+
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
55+
assert_name_is("testrepo/new.txt");
56+
}
57+
58+
void test_checkout_icase__overwrites_links_for_files(void)
59+
{
60+
cl_must_pass(p_symlink("../tmp", "testrepo/NEW.txt"));
61+
62+
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
63+
64+
cl_assert(!git_path_exists("tmp"));
65+
assert_name_is("testrepo/new.txt");
66+
}
67+
68+
void test_checkout_icase__overwites_folders_for_files(void)
69+
{
70+
cl_must_pass(p_mkdir("testrepo/NEW.txt", 0777));
71+
72+
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
73+
74+
assert_name_is("testrepo/new.txt");
75+
cl_assert(!git_path_isdir("testrepo/new.txt"));
76+
}
77+
78+
void test_checkout_icase__overwrites_files_for_folders(void)
79+
{
80+
cl_git_write2file("testrepo/A", "neue file\n", 10, \
81+
O_WRONLY | O_CREAT | O_TRUNC, 0644);
82+
83+
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
84+
assert_name_is("testrepo/a");
85+
cl_assert(git_path_isdir("testrepo/a"));
86+
}
87+
88+
void test_checkout_icase__overwrites_links_for_folders(void)
89+
{
90+
cl_must_pass(p_symlink("..", "testrepo/A"));
91+
92+
cl_git_pass(git_checkout_tree(repo, obj, &checkout_opts));
93+
94+
cl_assert(!git_path_exists("b.txt"));
95+
assert_name_is("testrepo/a");
96+
}
97+

tests/checkout/index.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,10 @@ void test_checkout_index__options_open_flags(void)
279279

280280
cl_git_mkfile("./testrepo/new.txt", "hi\n");
281281

282-
opts.checkout_strategy = GIT_CHECKOUT_SAFE_CREATE;
282+
opts.checkout_strategy =
283+
GIT_CHECKOUT_FORCE | GIT_CHECKOUT_DONT_REMOVE_EXISTING;
283284
opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND;
284285

285-
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
286286
cl_git_pass(git_checkout_index(g_repo, NULL, &opts));
287287

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

0 commit comments

Comments
 (0)