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