Skip to content

Commit 8b6e289

Browse files
committed
index: fix adding index entries with conflicting files
When adding an index entry "a/b/c" while an index entry "a/b" already exists, git will happily remove "a/b/c" and only add the new index entry: $ git init test Initialized empty Git repository in /tmp/test.repo/test/.git/ $ touch x $ git add x $ rm x $ mkdir x $ touch x/y $ git add x/y $ git status A x/y The other way round, adding an index entry "a/b" with an entry "a/b/c" already existing is equivalent, where git will remove "a/b/c" and add "a/b". In contrast, libgit2 will currently fail to add these properly and instead complain about the entry appearing as both a file and a directory. This is a programming error, though: our current code already tries to detect and, in the case of `git_index_add`, to automatically replace such index entries. Funnily enough, we already remove the conflicting index entries, but instead of adding the new entry we then bail out afterwards. This leaves callers with the worst of both worlds: we both remove the old entry but fail to add the new one. The root cause is weird semantics of the `has_file_name` and `has_dir_name` functions. While these functions only sound like they are responsible for detecting such conflicts, they will also already remove them in case where its `ok_to_replace` parameter is set. But even if we tell it to replace such entries, it will return an error code. Fix the error by returning success in case where the entries have been replaced. Fix an already existing test which tested for wrong behaviour. Note that the test didn't notice that the resulting tree had no entries. Thus it is fine to change existing behaviour here, as the previous result could've let to silently loosing data. Also add a new test that verifies behaviour in the reverse conflicting case.
1 parent 923317d commit 8b6e289

File tree

2 files changed

+57
-16
lines changed

2 files changed

+57
-16
lines changed

src/index.c

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,6 @@ static int index_entry_dup_nocache(
10961096
static int has_file_name(git_index *index,
10971097
const git_index_entry *entry, size_t pos, int ok_to_replace)
10981098
{
1099-
int retval = 0;
11001099
size_t len = strlen(entry->path);
11011100
int stage = GIT_IDXENTRY_STAGE(entry);
11021101
const char *name = entry->path;
@@ -1112,14 +1111,13 @@ static int has_file_name(git_index *index,
11121111
continue;
11131112
if (p->path[len] != '/')
11141113
continue;
1115-
retval = -1;
11161114
if (!ok_to_replace)
1117-
break;
1115+
return -1;
11181116

11191117
if (index_remove_entry(index, --pos) < 0)
11201118
break;
11211119
}
1122-
return retval;
1120+
return 0;
11231121
}
11241122

11251123
/*
@@ -1129,7 +1127,6 @@ static int has_file_name(git_index *index,
11291127
static int has_dir_name(git_index *index,
11301128
const git_index_entry *entry, int ok_to_replace)
11311129
{
1132-
int retval = 0;
11331130
int stage = GIT_IDXENTRY_STAGE(entry);
11341131
const char *name = entry->path;
11351132
const char *slash = name + strlen(name);
@@ -1141,14 +1138,13 @@ static int has_dir_name(git_index *index,
11411138
if (*--slash == '/')
11421139
break;
11431140
if (slash <= entry->path)
1144-
return retval;
1141+
return 0;
11451142
}
11461143
len = slash - name;
11471144

11481145
if (!index_find(&pos, index, name, len, stage)) {
1149-
retval = -1;
11501146
if (!ok_to_replace)
1151-
break;
1147+
return -1;
11521148

11531149
if (index_remove_entry(index, pos) < 0)
11541150
break;
@@ -1169,20 +1165,18 @@ static int has_dir_name(git_index *index,
11691165
break; /* not our subdirectory */
11701166

11711167
if (GIT_IDXENTRY_STAGE(&p->entry) == stage)
1172-
return retval;
1168+
return 0;
11731169
}
11741170
}
11751171

1176-
return retval;
1172+
return 0;
11771173
}
11781174

11791175
static int check_file_directory_collision(git_index *index,
11801176
git_index_entry *entry, size_t pos, int ok_to_replace)
11811177
{
1182-
int retval = has_file_name(index, entry, pos, ok_to_replace);
1183-
retval = retval + has_dir_name(index, entry, ok_to_replace);
1184-
1185-
if (retval) {
1178+
if (has_file_name(index, entry, pos, ok_to_replace) < 0 ||
1179+
has_dir_name(index, entry, ok_to_replace) < 0) {
11861180
giterr_set(GITERR_INDEX,
11871181
"'%s' appears as both a file and a directory", entry->path);
11881182
return -1;

tests/index/collision.c

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ void test_index_collision__cleanup(void)
2323
cl_git_sandbox_cleanup();
2424
}
2525

26-
void test_index_collision__add(void)
26+
void test_index_collision__add_blob_with_conflicting_file(void)
2727
{
2828
git_index_entry entry;
29+
git_tree_entry *tentry;
2930
git_oid tree_id;
3031
git_tree *tree;
3132

@@ -39,13 +40,59 @@ void test_index_collision__add(void)
3940
entry.path = "a/b";
4041
cl_git_pass(git_index_add(g_index, &entry));
4142

43+
/* Check a/b exists here */
44+
cl_git_pass(git_index_write_tree(&tree_id, g_index));
45+
cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
46+
cl_git_pass(git_tree_entry_bypath(&tentry, tree, "a/b"));
47+
git_tree_entry_free(tentry);
48+
git_tree_free(tree);
49+
4250
/* create a tree/blob collision */
4351
entry.path = "a/b/c";
44-
cl_git_fail(git_index_add(g_index, &entry));
52+
cl_git_pass(git_index_add(g_index, &entry));
4553

54+
/* a/b should now be a tree and a/b/c a blob */
4655
cl_git_pass(git_index_write_tree(&tree_id, g_index));
4756
cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
57+
cl_git_pass(git_tree_entry_bypath(&tentry, tree, "a/b/c"));
58+
git_tree_entry_free(tentry);
59+
git_tree_free(tree);
60+
}
61+
62+
void test_index_collision__add_blob_with_conflicting_dir(void)
63+
{
64+
git_index_entry entry;
65+
git_tree_entry *tentry;
66+
git_oid tree_id;
67+
git_tree *tree;
4868

69+
memset(&entry, 0, sizeof(entry));
70+
entry.ctime.seconds = 12346789;
71+
entry.mtime.seconds = 12346789;
72+
entry.mode = 0100644;
73+
entry.file_size = 0;
74+
git_oid_cpy(&entry.id, &g_empty_id);
75+
76+
entry.path = "a/b/c";
77+
cl_git_pass(git_index_add(g_index, &entry));
78+
79+
/* Check a/b/c exists here */
80+
cl_git_pass(git_index_write_tree(&tree_id, g_index));
81+
cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
82+
cl_git_pass(git_tree_entry_bypath(&tentry, tree, "a/b/c"));
83+
git_tree_entry_free(tentry);
84+
git_tree_free(tree);
85+
86+
/* create a blob/tree collision */
87+
entry.path = "a/b";
88+
cl_git_pass(git_index_add(g_index, &entry));
89+
90+
/* a/b should now be a tree and a/b/c a blob */
91+
cl_git_pass(git_index_write_tree(&tree_id, g_index));
92+
cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id));
93+
cl_git_pass(git_tree_entry_bypath(&tentry, tree, "a/b"));
94+
cl_git_fail(git_tree_entry_bypath(&tentry, tree, "a/b/c"));
95+
git_tree_entry_free(tentry);
4996
git_tree_free(tree);
5097
}
5198

0 commit comments

Comments
 (0)