]>
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; | |
5b05f954 CMN |
136 | struct timeval times[2]; |
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 | ||
165 | entry->ctime.seconds = (git_time_t)statbuf.st_ctime; | |
bb13d391 | 166 | entry->mtime.seconds = (git_time_t)statbuf.st_mtime; |
e7de893e AR |
167 | #if !defined(GIT_WIN32) && !defined(__APPLE__) |
168 | /* Apple and Windows doesn't provide these struct stat fields. */ | |
169 | entry->ctime.nanoseconds = statbuf.st_ctim.tv_nsec; | |
170 | entry->mtime.nanoseconds = statbuf.st_mtim.tv_nsec; | |
171 | #else | |
172 | entry->ctime.nanoseconds = 0; | |
bb13d391 | 173 | entry->mtime.nanoseconds = 0; |
e7de893e | 174 | #endif |
bb13d391 ET |
175 | entry->dev = statbuf.st_dev; |
176 | entry->ino = statbuf.st_ino; | |
177 | entry->uid = statbuf.st_uid; | |
178 | entry->gid = statbuf.st_gid; | |
179 | entry->file_size = statbuf.st_size; | |
180 | } | |
181 | ||
182 | git_buf_free(&path); | |
183 | } | |
184 | ||
b60149ec ET |
185 | static void stage_random_files(char *files[]) |
186 | { | |
187 | char *filename; | |
188 | size_t i; | |
189 | ||
190 | write_files(files); | |
191 | ||
192 | for (i = 0, filename = files[i]; filename; filename = files[++i]) | |
193 | cl_git_pass(git_index_add_bypath(repo_index, filename)); | |
194 | } | |
195 | ||
16eb8b7c ET |
196 | static void stage_content(char *content[]) |
197 | { | |
198 | git_reference *head; | |
199 | git_object *head_object; | |
16eb8b7c ET |
200 | git_buf path = GIT_BUF_INIT; |
201 | char *filename, *text; | |
202 | size_t i; | |
203 | ||
204 | cl_git_pass(git_repository_head(&head, repo)); | |
205 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); | |
23a17803 | 206 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
16eb8b7c ET |
207 | |
208 | for (i = 0, filename = content[i], text = content[++i]; | |
209 | filename && text; | |
210 | filename = content[++i], text = content[++i]) { | |
211 | ||
212 | git_buf_clear(&path); | |
213 | ||
214 | cl_git_pass(git_buf_printf(&path, "%s/%s", TEST_REPO_PATH, filename)); | |
215 | ||
216 | cl_git_mkfile(path.ptr, text); | |
217 | cl_git_pass(git_index_add_bypath(repo_index, filename)); | |
218 | } | |
219 | ||
16eb8b7c ET |
220 | git_object_free(head_object); |
221 | git_reference_free(head); | |
222 | git_buf_free(&path); | |
223 | } | |
224 | ||
b60149ec ET |
225 | static int merge_dirty_files(char *dirty_files[]) |
226 | { | |
227 | git_reference *head; | |
228 | git_object *head_object; | |
b60149ec ET |
229 | int error; |
230 | ||
231 | cl_git_pass(git_repository_head(&head, repo)); | |
232 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); | |
23a17803 | 233 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
b60149ec ET |
234 | |
235 | write_files(dirty_files); | |
236 | ||
a4e2c36a | 237 | error = merge_branch(); |
b60149ec | 238 | |
b60149ec ET |
239 | git_object_free(head_object); |
240 | git_reference_free(head); | |
241 | ||
242 | return error; | |
243 | } | |
244 | ||
bb13d391 ET |
245 | static int merge_differently_filtered_files(char *files[]) |
246 | { | |
247 | git_reference *head; | |
248 | git_object *head_object; | |
bb13d391 ET |
249 | int error; |
250 | ||
251 | cl_git_pass(git_repository_head(&head, repo)); | |
252 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); | |
23a17803 | 253 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
bb13d391 | 254 | |
fef5344c ET |
255 | /* Emulate checkout with a broken or misconfigured filter: modify some |
256 | * files on-disk and then update the index with the updated file size | |
257 | * and time, as if some filter applied them. These files should not be | |
258 | * treated as dirty since we created them. | |
259 | * | |
260 | * (Make sure to update the index stamp to defeat racy-git protections | |
261 | * trying to sanity check the files in the index; those would rehash the | |
262 | * files, showing them as dirty, the exact mechanism we're trying to avoid.) | |
263 | */ | |
264 | ||
bb13d391 ET |
265 | write_files(files); |
266 | hack_index(files); | |
267 | ||
268 | cl_git_pass(git_index_write(repo_index)); | |
269 | ||
a4e2c36a | 270 | error = merge_branch(); |
bb13d391 | 271 | |
bb13d391 ET |
272 | git_object_free(head_object); |
273 | git_reference_free(head); | |
274 | ||
275 | return error; | |
276 | } | |
277 | ||
b60149ec | 278 | static int merge_staged_files(char *staged_files[]) |
d9fdee6e | 279 | { |
b60149ec | 280 | stage_random_files(staged_files); |
a4e2c36a | 281 | return merge_branch(); |
b60149ec ET |
282 | } |
283 | ||
284 | void test_merge_workdir_dirty__unaffected_dirty_files_allowed(void) | |
285 | { | |
286 | char **files; | |
287 | size_t i; | |
288 | ||
289 | for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) | |
290 | cl_git_pass(merge_dirty_files(files)); | |
291 | } | |
292 | ||
26564d80 ET |
293 | void test_merge_workdir_dirty__unstaged_deletes_maintained(void) |
294 | { | |
295 | git_reference *head; | |
296 | git_object *head_object; | |
297 | ||
298 | cl_git_pass(git_repository_head(&head, repo)); | |
299 | cl_git_pass(git_reference_peel(&head_object, head, GIT_OBJ_COMMIT)); | |
23a17803 | 300 | cl_git_pass(git_reset(repo, head_object, GIT_RESET_HARD, NULL)); |
26564d80 ET |
301 | |
302 | cl_git_pass(p_unlink("merge-resolve/unchanged.txt")); | |
303 | ||
a4e2c36a | 304 | cl_git_pass(merge_branch()); |
26564d80 ET |
305 | |
306 | git_object_free(head_object); | |
307 | git_reference_free(head); | |
308 | } | |
309 | ||
b60149ec ET |
310 | void test_merge_workdir_dirty__affected_dirty_files_disallowed(void) |
311 | { | |
312 | char **files; | |
313 | size_t i; | |
314 | ||
315 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
316 | cl_git_fail(merge_dirty_files(files)); | |
317 | } | |
318 | ||
319 | void test_merge_workdir_dirty__staged_files_in_index_disallowed(void) | |
320 | { | |
321 | char **files; | |
322 | size_t i; | |
323 | ||
324 | for (i = 0, files = unaffected[i]; files[0]; files = unaffected[++i]) | |
325 | cl_git_fail(merge_staged_files(files)); | |
326 | ||
327 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
328 | cl_git_fail(merge_staged_files(files)); | |
329 | } | |
16eb8b7c ET |
330 | |
331 | void test_merge_workdir_dirty__identical_staged_files_allowed(void) | |
332 | { | |
16eb8b7c ET |
333 | char **content; |
334 | size_t i; | |
335 | ||
336 | set_core_autocrlf_to(repo, false); | |
337 | ||
338 | for (i = 0, content = result_contents[i]; content[0]; content = result_contents[++i]) { | |
339 | stage_content(content); | |
340 | ||
341 | git_index_write(repo_index); | |
a4e2c36a | 342 | cl_git_pass(merge_branch()); |
16eb8b7c ET |
343 | } |
344 | } | |
bb13d391 ET |
345 | |
346 | void test_merge_workdir_dirty__honors_cache(void) | |
347 | { | |
348 | char **files; | |
349 | size_t i; | |
350 | ||
351 | for (i = 0, files = affected[i]; files[0]; files = affected[++i]) | |
352 | cl_git_pass(merge_differently_filtered_files(files)); | |
353 | } |