Commit 25423d03b826f9647e72a56b14e08967d92ae479

Russell Belfer 2013-01-09T16:07:54

Support case insensitive tree iterators and status This makes tree iterators directly support case insensitivity by using a secondary index that can be sorted by icase. Also, this fixes the ambiguity check in the git_status_file API to also be case insensitive. Lastly, this adds new test cases for case insensitive range boundary checking for all types of iterators. With this change, it should be possible to deprecate the spool and sort iterator, but I haven't done that yet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
diff --git a/src/iterator.c b/src/iterator.c
index a9df39b..0b6a4fc 100644
--- a/src/iterator.c
+++ b/src/iterator.c
@@ -31,6 +31,7 @@
 	(P)->base.end     = end ? git__strdup(end) : NULL; \
 	if ((start && !(P)->base.start) || (end && !(P)->base.end)) { \
 		git__free(P); return -1; } \
+	(P)->base.prefixcomp = git__prefixcmp; \
 	} while (0)
 
 static int iterator__reset_range(
@@ -75,6 +76,9 @@ static int iterator_update_ignore_case(
 	else if (ignore_case == 0)
 		iter->flags = (iter->flags & ~GIT_ITERATOR_IGNORE_CASE);
 
+	iter->prefixcomp = ((iter->flags & GIT_ITERATOR_IGNORE_CASE) != 0) ?
+		git__prefixcmp_icase : git__prefixcmp;
+
 	return error;
 }
 
@@ -141,7 +145,10 @@ struct tree_iterator_frame {
 	tree_iterator_frame *next, *prev;
 	git_tree *tree;
 	char *start;
+	size_t startlen;
 	size_t index;
+	void **icase_map;
+	void *icase_data[GIT_FLEX_ARRAY];
 };
 
 typedef struct {
@@ -155,7 +162,13 @@ typedef struct {
 
 GIT_INLINE(const git_tree_entry *)tree_iterator__tree_entry(tree_iterator *ti)
 {
-	return git_tree_entry_byindex(ti->stack->tree, ti->stack->index);
+	tree_iterator_frame *tf = ti->stack;
+
+	if (tf->index >= git_tree_entrycount(tf->tree))
+		return NULL;
+
+	return git_tree_entry_byindex(
+		tf->tree, tf->icase_map ? (size_t)tf->icase_map[tf->index] : tf->index);
 }
 
 static char *tree_iterator__current_filename(
@@ -174,7 +187,10 @@ static void tree_iterator__free_frame(tree_iterator_frame *tf)
 {
 	if (!tf)
 		return;
+
 	git_tree_free(tf->tree);
+	tf->tree = NULL;
+
 	git__free(tf);
 }
 
@@ -220,7 +236,7 @@ static int tree_iterator__current(
 	if (ti->entry.path == NULL)
 		return -1;
 
-	if (ti->base.end && git__prefixcmp(ti->entry.path, ti->base.end) > 0)
+	if (ti->base.end && ti->base.prefixcomp(ti->entry.path, ti->base.end) > 0)
 		return tree_iterator__to_end(ti);
 
 	if (entry)
@@ -234,10 +250,50 @@ static int tree_iterator__at_end(git_iterator *self)
 	return (tree_iterator__tree_entry((tree_iterator *)self) == NULL);
 }
 
+static int tree_iterator__icase_map_cmp(const void *a, const void *b, void *data)
+{
+	git_tree *tree = data;
+	const git_tree_entry *te1 = git_tree_entry_byindex(tree, (size_t)a);
+	const git_tree_entry *te2 = git_tree_entry_byindex(tree, (size_t)b);
+	return te1 ? (te2 ? git_tree_entry_icmp(te1, te2) : 1) : -1;
+}
+
+static int tree_iterator__frame_start_icmp(const void *key, const void *element)
+{
+	const tree_iterator_frame *tf = (const tree_iterator_frame *)key;
+	const git_tree_entry *te = git_tree_entry_byindex(tf->tree, (size_t)element);
+
+	return memcmp(tf->start, te->filename, min(tf->startlen, te->filename_len));
+}
+
+static void tree_iterator__frame_seek_start(tree_iterator_frame *tf)
+{
+	if (!tf->start)
+		tf->index = 0;
+	else if (!tf->icase_map)
+		tf->index = git_tree__prefix_position(tf->tree, tf->start);
+	else {
+		if (!git__bsearch(
+				tf->icase_map, git_tree_entrycount(tf->tree),
+				tf, tree_iterator__frame_start_icmp, &tf->index))
+		{
+			while (tf->index > 0) {
+				/* move back while previous entry is still prefixed */
+				if (tree_iterator__frame_start_icmp(
+						tf, (const void *)(tf->index - 1)))
+					break;
+				tf->index--;
+			}
+		}
+	}
+}
+
 static tree_iterator_frame *tree_iterator__alloc_frame(
-	git_tree *tree, char *start)
+	tree_iterator *ti, git_tree *tree, char *start)
 {
-	tree_iterator_frame *tf = git__calloc(1, sizeof(tree_iterator_frame));
+	size_t i, max_i = git_tree_entrycount(tree);
+	tree_iterator_frame *tf =
+		git__calloc(1, sizeof(tree_iterator_frame) + max_i * sizeof(void *));
 	if (!tf)
 		return NULL;
 
@@ -245,9 +301,24 @@ static tree_iterator_frame *tree_iterator__alloc_frame(
 
 	if (start && *start) {
 		tf->start = start;
-		tf->index = git_tree__prefix_position(tree, start);
+		tf->startlen = strlen(start);
 	}
 
+	if (!max_i)
+		return tf;
+
+	if ((ti->base.flags & GIT_ITERATOR_IGNORE_CASE) != 0) {
+		tf->icase_map = tf->icase_data;
+
+		for (i = 0; i < max_i; ++i)
+			tf->icase_map[i] = (void *)i;
+
+		git__tsort_r(
+			tf->icase_map, max_i, tree_iterator__icase_map_cmp, tf->tree);
+	}
+
+	tree_iterator__frame_seek_start(tf);
+
 	return tf;
 }
 
@@ -265,7 +336,7 @@ static int tree_iterator__expand_tree(tree_iterator *ti)
 
 		/* check that we have not passed the range end */
 		if (ti->base.end != NULL &&
-			git__prefixcmp(ti->path.ptr, ti->base.end) > 0)
+			ti->base.prefixcomp(ti->path.ptr, ti->base.end) > 0)
 			return tree_iterator__to_end(ti);
 
 		if ((error = git_tree_lookup(&subtree, ti->base.repo, &te->oid)) < 0)
@@ -275,14 +346,13 @@ static int tree_iterator__expand_tree(tree_iterator *ti)
 
 		/* apply range start to new frame if relevant */
 		if (ti->stack->start &&
-			git__prefixcmp(ti->stack->start, te->filename) == 0)
+			ti->base.prefixcomp(ti->stack->start, te->filename) == 0)
 		{
-			size_t namelen = strlen(te->filename);
-			if (ti->stack->start[namelen] == '/')
-				relpath = ti->stack->start + namelen + 1;
+			if (ti->stack->start[te->filename_len] == '/')
+				relpath = ti->stack->start + te->filename_len + 1;
 		}
 
-		if ((tf = tree_iterator__alloc_frame(subtree, relpath)) == NULL)
+		if ((tf = tree_iterator__alloc_frame(ti, subtree, relpath)) == NULL)
 			return -1;
 
 		tf->next  = ti->stack;
@@ -311,8 +381,9 @@ static int tree_iterator__advance(
 	}
 
 	while (1) {
-		te = git_tree_entry_byindex(ti->stack->tree, ++ti->stack->index);
-		if (te != NULL)
+		++ti->stack->index;
+
+		if ((te = tree_iterator__tree_entry(ti)) != NULL)
 			break;
 
 		if (!tree_iterator__pop_frame(ti))
@@ -362,8 +433,8 @@ static int tree_iterator__reset(
 	if (iterator__reset_range(self, start, end) < 0)
 		return -1;
 
-	ti->stack->index =
-		git_tree__prefix_position(ti->stack->tree, ti->base.start);
+	/* reset start position */
+	tree_iterator__frame_seek_start(ti->stack);
 
 	git_buf_clear(&ti->path);
 	ti->path_has_filename = false;
@@ -394,10 +465,7 @@ int git_iterator_for_tree_range(
 	if ((error = iterator_update_ignore_case((git_iterator *)ti, flags)) < 0)
 		goto fail;
 
-	/* TODO: implement icase support natively in tree iterators */
-	ti->base.flags = (ti->base.flags & ~GIT_ITERATOR_IGNORE_CASE);
-
-	ti->stack = ti->tail = tree_iterator__alloc_frame(tree, ti->base.start);
+	ti->stack = ti->tail = tree_iterator__alloc_frame(ti, tree, ti->base.start);
 
 	if ((error = tree_iterator__expand_tree(ti)) < 0)
 		goto fail;
@@ -447,7 +515,7 @@ static void index_iterator__skip_conflicts(
 
 		if (ie == NULL ||
 			(ii->base.end != NULL &&
-			ITERATOR_PREFIXCMP(ii->base, ie->path, ii->base.end) > 0)) {
+			 ii->base.prefixcomp(ie->path, ii->base.end) > 0)) {
 			ii->current = entrycount;
 			break;
 		}
@@ -513,8 +581,10 @@ int git_iterator_for_index_range(
 	ITERATOR_BASE_INIT(ii, index, INDEX);
 
 	ii->base.repo = git_index_owner(index);
-	if (index->ignore_case)
+	if (index->ignore_case) {
 		ii->base.flags |= GIT_ITERATOR_IGNORE_CASE;
+		ii->base.prefixcomp = git__prefixcmp_icase;
+	}
 	ii->index = index;
 	GIT_REFCOUNT_INC(index);
 
@@ -772,8 +842,8 @@ static int workdir_iterator__update_entry(workdir_iterator *wi)
 	if (git_buf_put(&wi->path, ps->path, ps->path_len) < 0)
 		return -1;
 
-	if (wi->base.end && ITERATOR_PREFIXCMP(
-			wi->base, wi->path.ptr + wi->root_len, wi->base.end) > 0)
+	if (wi->base.end &&
+		wi->base.prefixcomp(wi->path.ptr + wi->root_len, wi->base.end) > 0)
 		return 0;
 
 	wi->entry.path = ps->path;
@@ -1064,10 +1134,14 @@ int git_iterator_current_parent_tree(
 	tree_iterator *ti = (tree_iterator *)iter;
 	tree_iterator_frame *tf;
 	const char *scan = parent_path;
+	int (*strncomp)(const char *a, const char *b, size_t sz);
 
 	if (iter->type != GIT_ITERATOR_TYPE_TREE || ti->stack == NULL)
 		goto notfound;
 
+	strncomp = ((iter->flags & GIT_ITERATOR_IGNORE_CASE) != 0) ?
+		git__strncasecmp : git__strncmp;
+
 	for (tf = ti->tail; tf != NULL; tf = tf->prev) {
 		const git_tree_entry *te;
 
@@ -1076,9 +1150,10 @@ int git_iterator_current_parent_tree(
 			return 0;
 		}
 
-		te = git_tree_entry_byindex(tf->tree, tf->index);
+		te = git_tree_entry_byindex(tf->tree,
+			tf->icase_map ? (size_t)tf->icase_map[tf->index] : tf->index);
 
-		if (strncmp(scan, te->filename, te->filename_len) != 0)
+		if (strncomp(scan, te->filename, te->filename_len) != 0)
 			goto notfound;
 
 		scan += te->filename_len;
@@ -1129,8 +1204,7 @@ int git_iterator_advance_into_directory(
 	return entry ? git_iterator_current(iter, entry) : 0;
 }
 
-int git_iterator_cmp(
-	git_iterator *iter, const char *path_prefix)
+int git_iterator_cmp(git_iterator *iter, const char *path_prefix)
 {
 	const git_index_entry *entry;
 
@@ -1143,7 +1217,7 @@ int git_iterator_cmp(
 	if (!path_prefix)
 		return -1;
 
-	return ITERATOR_PREFIXCMP(*iter, entry->path, path_prefix);
+	return iter->prefixcomp(entry->path, path_prefix);
 }
 
 int git_iterator_current_workdir_path(git_iterator *iter, git_buf **path)
diff --git a/src/iterator.h b/src/iterator.h
index 67e8a42..a9bccfc 100644
--- a/src/iterator.h
+++ b/src/iterator.h
@@ -47,6 +47,7 @@ struct git_iterator {
 	git_repository *repo;
 	char *start;
 	char *end;
+	int (*prefixcomp)(const char *str, const char *prefix);
 	unsigned int flags;
 };
 
diff --git a/src/status.c b/src/status.c
index 5b89e8c..282cb39 100644
--- a/src/status.c
+++ b/src/status.c
@@ -196,19 +196,23 @@ struct status_file_info {
 	char *expected;
 	unsigned int count;
 	unsigned int status;
+	int fnm_flags;
 	int ambiguous;
 };
 
 static int get_one_status(const char *path, unsigned int status, void *data)
 {
 	struct status_file_info *sfi = data;
+	int (*strcomp)(const char *a, const char *b);
 
 	sfi->count++;
 	sfi->status = status;
 
+	strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;
+
 	if (sfi->count > 1 ||
-		(strcmp(sfi->expected, path) != 0 &&
-		p_fnmatch(sfi->expected, path, 0) != 0))
+		(strcomp(sfi->expected, path) != 0 &&
+		 p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
 	{
 		sfi->ambiguous = true;
 		return GIT_EAMBIGUOUS;
@@ -225,11 +229,17 @@ int git_status_file(
 	int error;
 	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
 	struct status_file_info sfi = {0};
+	git_index *index;
 
 	assert(status_flags && repo && path);
 
+	if ((error = git_repository_index__weakptr(&index, repo)) < 0)
+		return error;
+
 	if ((sfi.expected = git__strdup(path)) == NULL)
 		return -1;
+	if (index->ignore_case)
+		sfi.fnm_flags = FNM_CASEFOLD;
 
 	opts.show  = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
 	opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
diff --git a/tests-clar/diff/iterator.c b/tests-clar/diff/iterator.c
index 846145a..566503b 100644
--- a/tests-clar/diff/iterator.c
+++ b/tests-clar/diff/iterator.c
@@ -756,18 +756,16 @@ void test_diff_iterator__workdir_builtin_ignores(void)
 	git_iterator_free(i);
 }
 
-static void check_first_through_third_range(
+static void check_wd_first_through_third_range(
 	git_repository *repo, const char *start, const char *end)
 {
 	git_iterator *i;
 	const git_index_entry *entry;
 	int idx;
-	static const char *expected[] = {
-		"FIRST", "second", "THIRD", NULL
-	};
+	static const char *expected[] = { "FIRST", "second", "THIRD", NULL };
 
 	cl_git_pass(git_iterator_for_workdir_range(
-		&i, repo, GIT_IGNORE_CASE, start, end));
+		&i, repo, GIT_ITERATOR_IGNORE_CASE, start, end));
 	cl_git_pass(git_iterator_current(i, &entry));
 
 	for (idx = 0; entry != NULL; ++idx) {
@@ -798,9 +796,114 @@ void test_diff_iterator__workdir_handles_icase_range(void)
 	cl_git_mkfile("empty_standard_repo/zafter", "whatever\n");
 	cl_git_mkfile("empty_standard_repo/Zlast", "whatever\n");
 
-	check_first_through_third_range(repo, "first", "third");
-	check_first_through_third_range(repo, "FIRST", "THIRD");
-	check_first_through_third_range(repo, "first", "THIRD");
-	check_first_through_third_range(repo, "FIRST", "third");
-	check_first_through_third_range(repo, "FirSt", "tHiRd");
+	check_wd_first_through_third_range(repo, "first", "third");
+	check_wd_first_through_third_range(repo, "FIRST", "THIRD");
+	check_wd_first_through_third_range(repo, "first", "THIRD");
+	check_wd_first_through_third_range(repo, "FIRST", "third");
+	check_wd_first_through_third_range(repo, "FirSt", "tHiRd");
+}
+
+static void check_tree_range(
+	git_repository *repo,
+	const char *start,
+	const char *end,
+	bool ignore_case,
+	int expected_count)
+{
+	git_tree *head;
+	git_iterator *i;
+	const git_index_entry *entry;
+	int count;
+
+	cl_git_pass(git_repository_head_tree(&head, repo));
+
+	cl_git_pass(git_iterator_for_tree_range(
+		&i, head,
+		ignore_case ? GIT_ITERATOR_IGNORE_CASE : GIT_ITERATOR_DONT_IGNORE_CASE,
+		start, end));
+
+	cl_git_pass(git_iterator_current(i, &entry));
+
+	for (count = 0; entry != NULL; ) {
+		++count;
+		cl_git_pass(git_iterator_advance(i, &entry));
+	}
+
+	cl_assert_equal_i(expected_count, count);
+
+	git_iterator_free(i);
+	git_tree_free(head);
+}
+
+void test_diff_iterator__tree_handles_icase_range(void)
+{
+	git_repository *repo;
+
+	repo = cl_git_sandbox_init("testrepo");
+
+	check_tree_range(repo, "B", "C", false, 0);
+	check_tree_range(repo, "B", "C", true, 1);
+	check_tree_range(repo, "a", "z", false, 3);
+	check_tree_range(repo, "a", "z", true, 4);
+}
+
+static void check_index_range(
+	git_repository *repo,
+	const char *start,
+	const char *end,
+	bool ignore_case,
+	int expected_count)
+{
+	git_index *index;
+	git_iterator *i;
+	const git_index_entry *entry;
+	int count, caps;
+	bool is_ignoring_case;
+
+	cl_git_pass(git_repository_index(&index, repo));
+
+	caps = git_index_caps(index);
+	is_ignoring_case = ((caps & GIT_INDEXCAP_IGNORE_CASE) != 0);
+
+	if (ignore_case != is_ignoring_case)
+		cl_git_pass(git_index_set_caps(index, caps ^ GIT_INDEXCAP_IGNORE_CASE));
+
+	cl_git_pass(git_iterator_for_index_range(&i, index, 0, start, end));
+
+	cl_assert(git_iterator_ignore_case(i) == ignore_case);
+
+	cl_git_pass(git_iterator_current(i, &entry));
+
+	for (count = 0; entry != NULL; ) {
+		++count;
+		cl_git_pass(git_iterator_advance(i, &entry));
+	}
+
+	cl_assert_equal_i(expected_count, count);
+
+	git_iterator_free(i);
+	git_index_free(index);
+}
+
+void test_diff_iterator__index_handles_icase_range(void)
+{
+	git_repository *repo;
+	git_index *index;
+	git_tree *head;
+
+	repo = cl_git_sandbox_init("testrepo");
+
+	/* reset index to match HEAD */
+	cl_git_pass(git_repository_head_tree(&head, repo));
+	cl_git_pass(git_repository_index(&index, repo));
+	cl_git_pass(git_index_read_tree(index, head));
+	cl_git_pass(git_index_write(index));
+	git_tree_free(head);
+	git_index_free(index);
+
+	/* do some ranged iterator checks toggling case sensitivity */
+	check_index_range(repo, "B", "C", false, 0);
+	check_index_range(repo, "B", "C", true, 1);
+	check_index_range(repo, "a", "z", false, 3);
+	check_index_range(repo, "a", "z", true, 4);
 }