]>
Commit | Line | Data |
---|---|---|
b60149ec ET |
1 | #include "clar_libgit2.h" |
2 | #include "git2/merge.h" | |
b60149ec | 3 | #include "merge.h" |
fef5344c | 4 | #include "index.h" |
b60149ec | 5 | #include "../merge_helpers.h" |
bb13d391 | 6 | #include "posix.h" |
b60149ec ET |
7 | |
8 | #define TEST_REPO_PATH "merge-resolve" | |
9 | #define MERGE_BRANCH_OID "7cb63eed597130ba4abb87b3e544b85021905520" | |
10 | ||
16eb8b7c ET |
11 | #define AUTOMERGEABLE_MERGED_FILE \ |
12 | "this file is changed in master\n" \ | |
13 | "this file is automergeable\n" \ | |
14 | "this file is automergeable\n" \ | |
15 | "this file is automergeable\n" \ | |
16 | "this file is automergeable\n" \ | |
17 | "this file is automergeable\n" \ | |
18 | "this file is automergeable\n" \ | |
19 | "this file is automergeable\n" \ | |
20 | "this file is changed in branch\n" | |
21 | ||
22 | #define CHANGED_IN_BRANCH_FILE \ | |
23 | "changed in branch\n" | |
24 | ||
b60149ec ET |
25 | static git_repository *repo; |
26 | static git_index *repo_index; | |
27 | ||
28 | static char *unaffected[][4] = { | |
29 | { "added-in-master.txt", NULL }, | |
30 | { "changed-in-master.txt", NULL }, | |
31 | { "unchanged.txt", NULL }, | |
32 | { "added-in-master.txt", "changed-in-master.txt", NULL }, | |
33 | { "added-in-master.txt", "unchanged.txt", NULL }, | |
34 | { "changed-in-master.txt", "unchanged.txt", NULL }, | |
35 | { "added-in-master.txt", "changed-in-master.txt", "unchanged.txt", NULL }, | |
36 | { "new_file.txt", NULL }, | |
37 | { "new_file.txt", "unchanged.txt", NULL }, | |
38 | { NULL }, | |
39 | }; | |
40 | ||
41 | static char *affected[][5] = { | |
42 | { "automergeable.txt", NULL }, | |
43 | { "changed-in-branch.txt", NULL }, | |
44 | { "conflicting.txt", NULL }, | |
45 | { "removed-in-branch.txt", NULL }, | |
46 | { "automergeable.txt", "changed-in-branch.txt", NULL }, | |
47 | { "automergeable.txt", "conflicting.txt", NULL }, | |
48 | { "automergeable.txt", "removed-in-branch.txt", NULL }, | |
49 | { "changed-in-branch.txt", "conflicting.txt", NULL }, | |
50 | { "changed-in-branch.txt", "removed-in-branch.txt", NULL }, | |
51 | { "conflicting.txt", "removed-in-branch.txt", NULL }, | |
52 | { "automergeable.txt", "changed-in-branch.txt", "conflicting.txt", NULL }, | |
53 | { "automergeable.txt", "changed-in-branch.txt", "removed-in-branch.txt", NULL }, | |
54 | { "automergeable.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, | |
55 | { "changed-in-branch.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, | |
56 | { "automergeable.txt", "changed-in-branch.txt", "conflicting.txt", "removed-in-branch.txt", NULL }, | |
57 | { NULL }, | |
58 | }; | |
59 | ||
16eb8b7c ET |
60 | static char *result_contents[4][6] = { |
61 | { "automergeable.txt", AUTOMERGEABLE_MERGED_FILE, NULL, NULL }, | |
62 | { "changed-in-branch.txt", CHANGED_IN_BRANCH_FILE, NULL, NULL }, | |
63 | { "automergeable.txt", AUTOMERGEABLE_MERGED_FILE, "changed-in-branch.txt", CHANGED_IN_BRANCH_FILE, NULL, NULL }, | |
64 | { NULL } | |
65 | }; | |
66 | ||
b60149ec ET |
67 | void test_merge_workdir_dirty__initialize(void) |
68 | { | |
69 | repo = cl_git_sandbox_init(TEST_REPO_PATH); | |
70 | git_repository_index(&repo_index, repo); | |
71 | } | |
72 | ||
73 | void test_merge_workdir_dirty__cleanup(void) | |
74 | { | |
75 | git_index_free(repo_index); | |
76 | cl_git_sandbox_cleanup(); | |
77 | } | |
78 | ||
79 | static void set_core_autocrlf_to(git_repository *repo, bool value) | |
80 | { | |
81 | git_config *cfg; | |
82 | ||
83 | cl_git_pass(git_repository_config(&cfg, repo)); | |
84 | cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", value)); | |
85 | ||
86 | git_config_free(cfg); | |
87 | } | |
88 | ||
a4e2c36a | 89 | static int merge_branch(void) |
b60149ec ET |
90 | { |
91 | git_oid their_oids[1]; | |
18b00406 | 92 | git_annotated_commit *their_head; |
5aa2ac6d | 93 | git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; |
02105a27 | 94 | git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; |
b60149ec ET |
95 | int error; |
96 | ||
97 | cl_git_pass(git_oid_fromstr(&their_oids[0], MERGE_BRANCH_OID)); | |
18b00406 | 98 | cl_git_pass(git_annotated_commit_lookup(&their_head, repo, &their_oids[0])); |
b60149ec | 99 | |
967f5a76 | 100 | checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; |
18b00406 | 101 | error = git_merge(repo, (const git_annotated_commit **)&their_head, 1, &merge_opts, &checkout_opts); |
b60149ec | 102 | |
18b00406 | 103 | git_annotated_commit_free(their_head); |
b60149ec ET |
104 | |
105 | return error; | |
106 | } | |
107 | ||
108 | static void write_files(char *files[]) | |
109 | { | |
110 | char *filename; | |
e579e0f7 | 111 | git_str path = GIT_STR_INIT, content = GIT_STR_INIT; |
b60149ec ET |
112 | size_t i; |
113 | ||
114 | for (i = 0, filename = files[i]; filename; filename = files[++i]) { | |
e579e0f7 MB |
115 | git_str_clear(&path); |
116 | git_str_clear(&content); | |
b60149ec | 117 | |
e579e0f7 MB |
118 | git_str_printf(&path, "%s/%s", TEST_REPO_PATH, filename); |
119 | git_str_printf(&content, "This is a dirty file in the working directory!\n\n" | |
b60149ec ET |
120 | "It will not be staged! Its filename is %s.\n", filename); |
121 | ||
122 | cl_git_mkfile(path.ptr, content.ptr); | |
123 | } | |
124 | ||
e579e0f7 MB |
125 | git_str_dispose(&path); |
126 | git_str_dispose(&content); | |
b60149ec ET |
127 | } |
128 | ||
bb13d391 ET |
129 | static void hack_index(char *files[]) |
130 | { | |
131 | char *filename; | |
132 | struct stat statbuf; | |
e579e0f7 | 133 | git_str path = GIT_STR_INIT; |
bb13d391 | 134 | git_index_entry *entry; |
35439f59 | 135 | struct p_timeval times[2]; |
5b05f954 | 136 | time_t now; |
bb13d391 ET |
137 | size_t i; |
138 | ||
139 | /* Update the index to suggest that checkout placed these files on | |
140 | * disk, keeping the object id but updating the cache, which will | |
141 | * emulate a Git implementation's different filter. | |
5b05f954 CMN |
142 | * |
143 | * We set the file's timestamp to before now to pretend that | |
144 | * it was an old checkout so we don't trigger the racy | |
145 | * protections would would check the content. | |
bb13d391 | 146 | */ |
5b05f954 CMN |
147 | |
148 | now = time(NULL); | |
149 | times[0].tv_sec = now - 5; | |
150 | times[0].tv_usec = 0; | |
151 | times[1].tv_sec = now - 5; | |
152 | times[1].tv_usec = 0; | |
153 | ||
bb13d391 | 154 | for (i = 0, filename = files[i]; filename; filename = files[++i]) { |
e579e0f7 | 155 | git_str_clear(&path); |
bb13d391 ET |
156 | |
157 | cl_assert(entry = (git_index_entry *) | |
158 | git_index_get_bypath(repo_index, filename, 0)); | |
159 | ||
e579e0f7 | 160 | cl_git_pass(git_str_printf(&path, "%s/%s", TEST_REPO_PATH, filename)); |
5b05f954 | 161 | cl_git_pass(p_utimes(path.ptr, times)); |
bb13d391 ET |
162 | cl_git_pass(p_stat(path.ptr, &statbuf)); |
163 | ||
263e674e ET |
164 | entry->ctime.seconds = (int32_t)statbuf.st_ctime; |
165 | entry->mtime.seconds = (int32_t)statbuf.st_mtime; | |
e9e6df2c | 166 | #if defined(GIT_USE_NSEC) |
6abdf52d ET |
167 | entry->ctime.nanoseconds = statbuf.st_ctime_nsec; |
168 | entry->mtime.nanoseconds = statbuf.st_mtime_nsec; | |
e7de893e AR |
169 | #else |
170 | entry->ctime.nanoseconds = 0; | |
bb13d391 | 171 | entry->mtime.nanoseconds = 0; |
e7de893e | 172 | #endif |
bb13d391 ET |
173 | entry->dev = statbuf.st_dev; |
174 | entry->ino = statbuf.st_ino; | |
175 | entry->uid = statbuf.st_uid; | |
176 | entry->gid = statbuf.st_gid; | |
263e674e | 177 | entry->file_size = (uint32_t)statbuf.st_size; |
bb13d391 ET |
178 | } |
179 | ||
e579e0f7 | 180 | git_str_dispose(&path); |
bb13d391 ET |
181 | } |
182 | ||
b60149ec ET |
183 | static void stage_random_files(char *files[]) |
184 | { | |
185 | char *filename; | |
186 | size_t i; | |
187 | ||
188 | write_files(files); | |
189 | ||
190 | for (i = 0, filename = files[i]; filename; filename = files[++i]) | |
191 | cl_git_pass(git_index_add_bypath(repo_index, filename)); | |
192 | } | |
193 | ||
16eb8b7c ET |
194 | static void stage_content(char *content[]) |
195 | { | |
196 | git_reference *head; | |
197 | git_object *head_object; | |
e579e0f7 | 198 | git_str path = GIT_STR_INIT; |
16eb8b7c ET |
199 | char *filename, *text; |
200 | size_t i; | |
201 | ||
202 | cl_git_pass(git_repository_head(&head, repo)); | |
ac3d33df | 203 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJECT_COMMIT)); |
23a17803 | 204 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
16eb8b7c ET |
205 | |
206 | for (i = 0, filename = content[i], text = content[++i]; | |
207 | filename && text; | |
208 | filename = content[++i], text = content[++i]) { | |
209 | ||
e579e0f7 | 210 | git_str_clear(&path); |
16eb8b7c | 211 | |
e579e0f7 | 212 | cl_git_pass(git_str_printf(&path, "%s/%s", TEST_REPO_PATH, filename)); |
16eb8b7c ET |
213 | |
214 | cl_git_mkfile(path.ptr, text); | |
215 | cl_git_pass(git_index_add_bypath(repo_index, filename)); | |
216 | } | |
217 | ||
16eb8b7c ET |
218 | git_object_free(head_object); |
219 | git_reference_free(head); | |
e579e0f7 | 220 | git_str_dispose(&path); |
16eb8b7c ET |
221 | } |
222 | ||
b60149ec ET |
223 | static int merge_dirty_files(char *dirty_files[]) |
224 | { | |
225 | git_reference *head; | |
226 | git_object *head_object; | |
b60149ec ET |
227 | int error; |
228 | ||
229 | cl_git_pass(git_repository_head(&head, repo)); | |
ac3d33df | 230 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJECT_COMMIT)); |
23a17803 | 231 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
b60149ec ET |
232 | |
233 | write_files(dirty_files); | |
234 | ||
a4e2c36a | 235 | error = merge_branch(); |
b60149ec | 236 | |
b60149ec ET |
237 | git_object_free(head_object); |
238 | git_reference_free(head); | |
239 | ||
240 | return error; | |
241 | } | |
242 | ||
bb13d391 ET |
243 | static int merge_differently_filtered_files(char *files[]) |
244 | { | |
245 | git_reference *head; | |
246 | git_object *head_object; | |
bb13d391 ET |
247 | int error; |
248 | ||
249 | cl_git_pass(git_repository_head(&head, repo)); | |
ac3d33df | 250 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJECT_COMMIT)); |
23a17803 | 251 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
bb13d391 | 252 | |
fef5344c ET |
253 | /* Emulate checkout with a broken or misconfigured filter: modify some |
254 | * files on-disk and then update the index with the updated file size | |
255 | * and time, as if some filter applied them. These files should not be | |
256 | * treated as dirty since we created them. | |
257 | * | |
258 | * (Make sure to update the index stamp to defeat racy-git protections | |
259 | * trying to sanity check the files in the index; those would rehash the | |
260 | * files, showing them as dirty, the exact mechanism we're trying to avoid.) | |
261 | */ | |
262 | ||
bb13d391 ET |
263 | write_files(files); |
264 | hack_index(files); | |
265 | ||
266 | cl_git_pass(git_index_write(repo_index)); | |
267 | ||
a4e2c36a | 268 | error = merge_branch(); |
bb13d391 | 269 | |
bb13d391 ET |
270 | git_object_free(head_object); |
271 | git_reference_free(head); | |
272 | ||
273 | return error; | |
274 | } | |
275 | ||
b60149ec | 276 | static int merge_staged_files(char *staged_files[]) |
ac3d33df | 277 | { |
b60149ec | 278 | stage_random_files(staged_files); |
a4e2c36a | 279 | return merge_branch(); |
b60149ec ET |
280 | } |
281 | ||
282 | void test_merge_workdir_dirty__unaffected_dirty_files_allowed(void) | |
283 | { | |
284 | char **files; | |
285 | size_t i; | |
286 | ||
287 | for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) | |
288 | cl_git_pass(merge_dirty_files(files)); | |
289 | } | |
290 | ||
26564d80 ET |
291 | void test_merge_workdir_dirty__unstaged_deletes_maintained(void) |
292 | { | |
293 | git_reference *head; | |
294 | git_object *head_object; | |
295 | ||
296 | cl_git_pass(git_repository_head(&head, repo)); | |
ac3d33df | 297 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJECT_COMMIT)); |
23a17803 | 298 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
26564d80 ET |
299 | |
300 | cl_git_pass(p_unlink("merge-resolve/unchanged.txt")); | |
301 | ||
a4e2c36a | 302 | cl_git_pass(merge_branch()); |
26564d80 ET |
303 | |
304 | git_object_free(head_object); | |
305 | git_reference_free(head); | |
306 | } | |
307 | ||
b60149ec ET |
308 | void test_merge_workdir_dirty__affected_dirty_files_disallowed(void) |
309 | { | |
310 | char **files; | |
311 | size_t i; | |
312 | ||
313 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
314 | cl_git_fail(merge_dirty_files(files)); | |
315 | } | |
316 | ||
317 | void test_merge_workdir_dirty__staged_files_in_index_disallowed(void) | |
318 | { | |
319 | char **files; | |
320 | size_t i; | |
321 | ||
322 | for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) | |
323 | cl_git_fail(merge_staged_files(files)); | |
324 | ||
325 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
326 | cl_git_fail(merge_staged_files(files)); | |
327 | } | |
16eb8b7c ET |
328 | |
329 | void test_merge_workdir_dirty__identical_staged_files_allowed(void) | |
330 | { | |
16eb8b7c ET |
331 | char **content; |
332 | size_t i; | |
333 | ||
334 | set_core_autocrlf_to(repo, false); | |
ac3d33df | 335 | |
16eb8b7c ET |
336 | for (i = 0, content = result_contents[i]; content[0]; content = result_contents[++i]) { |
337 | stage_content(content); | |
338 | ||
ac3d33df | 339 | cl_git_pass(git_index_write(repo_index)); |
a4e2c36a | 340 | cl_git_pass(merge_branch()); |
16eb8b7c ET |
341 | } |
342 | } | |
bb13d391 ET |
343 | |
344 | void test_merge_workdir_dirty__honors_cache(void) | |
345 | { | |
346 | char **files; | |
347 | size_t i; | |
348 | ||
349 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
350 | cl_git_pass(merge_differently_filtered_files(files)); | |
351 | } |