Skip to content

Commit 64cab59

Browse files
author
Junio C Hamano
committed
apply: do not get confused by symlinks in the middle
HPA noticed that git-rebase fails when changes involve symlinks in the middle of the hierarchy. Consider: * The tree state before the patch is applied has arch/x86_64/boot as a symlink pointing at ../i386/boot/ * The patch tries to remove arch/x86_64/boot symlink, and create bunch of files there: .gitignore, Makefile, etc. git-apply tries to be careful while applying patches; it never touches the working tree until it is convinced that the patch would apply cleanly. One of the check it does is that when it knows a path is going to be created by the patch, it runs lstat() on the path to make sure it does not exist. This leads to a false alarm. Because we do not touch the working tree before all the check passes, when we try to make sure that arch/x86_64/boot/.gitignore does not exist yet, we haven't removed the arch/x86_64/boot symlink. The lstat() check ends up seeing arch/i386/boot/.gitignore through the yet-to-be-removed symlink, and says "Hey, you already have a file there, but what you fed me is a patch to create a new file. I am not going to clobber what you have in the working tree." We have similar checks to see a file we are going to modify does exist and match the preimage of the diff, which is done by directly opening and reading the file. For a file we are going to delete, we make sure that it does exist and matches what is going to be removed (a removal patch records the full preimage, so we check what you have in your working tree matches it in full -- otherwise we would risk losing your local changes), which again is done by directly opening and reading the file. These checks need to be adjusted so that they are not fooled by symlinks in the middle. - To make sure something does not exist, first lstat(). If it does not exist, it does not, so be happy. If it _does_, we might be getting fooled by a symlink in the middle, so break leading paths and see if there are symlinks involved. When we are checking for a path a/b/c/d, if any of a, a/b, a/b/c is a symlink, then a/b/c/d does _NOT_ exist, for the purpose of our test. This would fix this particular case you saw, and would not add extra overhead in the usual case. - To make sure something already exists, first lstat(). If it does not exist, barf (up to this, we already do). Even if it does seem to exist, we might be getting fooled by a symlink in the middle, so make sure leading paths are not symlinks. This would make the normal codepath much more expensive for deep trees, which is a bit worrisome. This patch implements the first side of the check "making sure it does not exist". The latter "making sure it exists" check is not done yet, so applying the patch in reverse would still fail, but we have to start from somewhere. Signed-off-by: Junio C Hamano <[email protected]>
1 parent f859c84 commit 64cab59

File tree

3 files changed

+84
-10
lines changed

3 files changed

+84
-10
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ LIB_OBJS = \
318318
write_or_die.o trace.o list-objects.o grep.o match-trees.o \
319319
alloc.o merge-file.o path-list.o help.o unpack-trees.o $(DIFF_OBJS) \
320320
color.o wt-status.o archive-zip.o archive-tar.o shallow.o utf8.o \
321-
convert.o attr.o decorate.o progress.o mailmap.o
321+
convert.o attr.o decorate.o progress.o mailmap.o symlinks.o
322322

323323
BUILTIN_OBJS = \
324324
builtin-add.o \

builtin-apply.c

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,6 +2009,29 @@ static int apply_data(struct patch *patch, struct stat *st, struct cache_entry *
20092009
return 0;
20102010
}
20112011

2012+
static int check_to_create_blob(const char *new_name, int ok_if_exists)
2013+
{
2014+
struct stat nst;
2015+
if (!lstat(new_name, &nst)) {
2016+
if (S_ISDIR(nst.st_mode) || ok_if_exists)
2017+
return 0;
2018+
/*
2019+
* A leading component of new_name might be a symlink
2020+
* that is going to be removed with this patch, but
2021+
* still pointing at somewhere that has the path.
2022+
* In such a case, path "new_name" does not exist as
2023+
* far as git is concerned.
2024+
*/
2025+
if (has_symlink_leading_path(new_name, NULL))
2026+
return 0;
2027+
2028+
return error("%s: already exists in working directory", new_name);
2029+
}
2030+
else if ((errno != ENOENT) && (errno != ENOTDIR))
2031+
return error("%s: %s", new_name, strerror(errno));
2032+
return 0;
2033+
}
2034+
20122035
static int check_patch(struct patch *patch, struct patch *prev_patch)
20132036
{
20142037
struct stat st;
@@ -2095,15 +2118,9 @@ static int check_patch(struct patch *patch, struct patch *prev_patch)
20952118
!ok_if_exists)
20962119
return error("%s: already exists in index", new_name);
20972120
if (!cached) {
2098-
struct stat nst;
2099-
if (!lstat(new_name, &nst)) {
2100-
if (S_ISDIR(nst.st_mode) || ok_if_exists)
2101-
; /* ok */
2102-
else
2103-
return error("%s: already exists in working directory", new_name);
2104-
}
2105-
else if ((errno != ENOENT) && (errno != ENOTDIR))
2106-
return error("%s: %s", new_name, strerror(errno));
2121+
int err = check_to_create_blob(new_name, ok_if_exists);
2122+
if (err)
2123+
return err;
21072124
}
21082125
if (!patch->new_mode) {
21092126
if (0 < patch->is_new)

t/t4122-apply-symlink-inside.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/sh
2+
3+
test_description='apply to deeper directory without getting fooled with symlink'
4+
. ./test-lib.sh
5+
6+
lecho () {
7+
for l_
8+
do
9+
echo "$l_"
10+
done
11+
}
12+
13+
test_expect_success setup '
14+
15+
mkdir -p arch/i386/boot arch/x86_64 &&
16+
lecho 1 2 3 4 5 >arch/i386/boot/Makefile &&
17+
ln -s ../i386/boot arch/x86_64/boot &&
18+
git add . &&
19+
test_tick &&
20+
git commit -m initial &&
21+
git branch test &&
22+
23+
rm arch/x86_64/boot &&
24+
mkdir arch/x86_64/boot &&
25+
lecho 2 3 4 5 6 >arch/x86_64/boot/Makefile &&
26+
git add . &&
27+
test_tick &&
28+
git commit -a -m second &&
29+
30+
git format-patch --binary -1 --stdout >test.patch
31+
32+
'
33+
34+
test_expect_success apply '
35+
36+
git checkout test &&
37+
git reset --hard && #### checkout seems to be buggy
38+
git diff --exit-code test &&
39+
git diff --exit-code --cached test &&
40+
git apply --index test.patch
41+
42+
'
43+
44+
test_expect_success 'check result' '
45+
46+
git diff --exit-code master &&
47+
git diff --exit-code --cached master &&
48+
test_tick &&
49+
git commit -m replay &&
50+
T1=$(git rev-parse "master^{tree}") &&
51+
T2=$(git rev-parse "HEAD^{tree}") &&
52+
test "z$T1" = "z$T2"
53+
54+
'
55+
56+
test_done
57+

0 commit comments

Comments
 (0)