Commit bd370b14fefdba3844a9bf0bbf87171ca48f49be

Russell Belfer 2011-12-30T15:00:14

Improved gitattributes macro implementation This updates to implementation of gitattribute macros to be much more similar to core git (albeit not 100%) and to handle expansion of macros within macros, etc. It also cleans up the refcounting usage with macros to be much cleaner. Also, this adds a new vector function `git_vector_insert_sorted()` which allows you to maintain a sorted list as you go. In order to write that function, this changes the function `git__bsearch()` to take a somewhat different set of parameters, although the core functionality is still the same.

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
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
diff --git a/src/attr_file.c b/src/attr_file.c
index a137905..fe8844e 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -12,17 +12,9 @@ static void git_attr_rule__clear(git_attr_rule *rule);
 
 int git_attr_cache__insert_macro(git_repository *repo, git_attr_rule *macro)
 {
-	unsigned int i;
-	git_attr_assignment *assign;
-
 	if (macro->assigns.length == 0)
 		return git__throw(GIT_EMISSINGOBJDATA, "git attribute macro with no values");
 
-	git_vector_foreach(&macro->assigns, i, assign) {
-		GIT_REFCOUNT_OWN(assign, macro);
-		GIT_REFCOUNT_INC(assign);
-	}
-
 	return git_hashtable_insert(
 		repo->attrcache.macros, macro->match.pattern, macro);
 }
@@ -358,7 +350,7 @@ static int sort_by_hash_and_name(const void *a_raw, const void *b_raw)
 		return strcmp(b->name, a->name);
 }
 
-static void free_assign(git_attr_assignment *assign)
+static void git_attr_assignment__free(git_attr_assignment *assign)
 {
 	git__free(assign->name);
 	assign->name = NULL;
@@ -371,6 +363,16 @@ static void free_assign(git_attr_assignment *assign)
 	git__free(assign);
 }
 
+static int merge_assignments(void **old_raw, void *new_raw)
+{
+	git_attr_assignment **old = (git_attr_assignment **)old_raw;
+	git_attr_assignment *new = (git_attr_assignment *)new_raw;
+
+	GIT_REFCOUNT_DEC(*old, git_attr_assignment__free);
+	*old = new;
+	return GIT_EEXISTS;
+}
+
 int git_attr_assignment__parse(
 	git_repository *repo,
 	git_vector *assigns,
@@ -382,6 +384,8 @@ int git_attr_assignment__parse(
 
 	assert(assigns && !assigns->length);
 
+	assigns->_cmp = sort_by_hash_and_name;
+
 	while (*scan && *scan != '\n' && error == GIT_SUCCESS) {
 		const char *name_start, *value_start;
 
@@ -395,6 +399,7 @@ int git_attr_assignment__parse(
 				error = GIT_ENOMEM;
 				break;
 			}
+			GIT_REFCOUNT_INC(assign);
 		}
 
 		assign->name_hash = 5381;
@@ -449,8 +454,8 @@ int git_attr_assignment__parse(
 			}
 		}
 
-		/* expand macros (if given a repo) */
-		if (repo != NULL) {
+		/* expand macros (if given a repo with a macro cache) */
+		if (repo != NULL && assign->value == GIT_ATTR_TRUE) {
 			git_attr_rule *macro =
 				git_hashtable_lookup(repo->attrcache.macros, assign->name);
 
@@ -458,30 +463,25 @@ int git_attr_assignment__parse(
 				unsigned int i;
 				git_attr_assignment *massign;
 
-				/* issue warning: if assign->value != GIT_ATTR_TRUE */
+				git_vector_foreach(&macro->assigns, i, massign) {
+					GIT_REFCOUNT_INC(massign);
 
-				git__free(assign->name);
-				assign->name = NULL;
-				if (assign->is_allocated) {
-					git__free((void *)assign->value);
-					assign->value = NULL;
-				}
+					error = git_vector_insert_sorted(
+						assigns, massign, &merge_assignments);
 
-				git_vector_foreach(&macro->assigns, i, massign) {
-					error = git_vector_insert(assigns, massign);
-					if (error != GIT_SUCCESS)
+					if (error == GIT_EEXISTS)
+						error = GIT_SUCCESS;
+					else if (error != GIT_SUCCESS)
 						break;
-					GIT_REFCOUNT_INC(&massign->rc);
 				}
-
-				/* continue to next assignment */
-				continue;
 			}
 		}
 
 		/* insert allocated assign into vector */
-		error = git_vector_insert(assigns, assign);
-		if (error < GIT_SUCCESS)
+		error = git_vector_insert_sorted(assigns, assign, &merge_assignments);
+		if (error == GIT_EEXISTS)
+			error = GIT_SUCCESS;
+		else if (error < GIT_SUCCESS)
 			break;
 
 		/* clear assign since it is now "owned" by the vector */
@@ -490,13 +490,9 @@ int git_attr_assignment__parse(
 
 	if (!assigns->length)
 		error = git__throw(GIT_ENOTFOUND, "No attribute assignments found for rule");
-	else {
-		assigns->_cmp = sort_by_hash_and_name;
-		git_vector_sort(assigns);
-	}
 
 	if (assign != NULL)
-		free_assign(assign);
+		git_attr_assignment__free(assign);
 
 	while (*scan && *scan != '\n') scan++;
 	if (*scan == '\n') scan++;
@@ -518,11 +514,8 @@ static void git_attr_rule__clear(git_attr_rule *rule)
 	rule->match.pattern = NULL;
 	rule->match.length = 0;
 
-	git_vector_foreach(&rule->assigns, i, assign) {
-		if (GIT_REFCOUNT_OWNER(assign) == rule)
-			GIT_REFCOUNT_OWN(assign, NULL);
-		GIT_REFCOUNT_DEC(assign, free_assign);
-	}
+	git_vector_foreach(&rule->assigns, i, assign)
+		GIT_REFCOUNT_DEC(assign, git_attr_assignment__free);
 
 	git_vector_free(&rule->assigns);
 }
diff --git a/src/util.c b/src/util.c
index b3af7ff..1ca9d85 100644
--- a/src/util.c
+++ b/src/util.c
@@ -348,22 +348,30 @@ uint32_t git__hash(const void *key, int len, uint32_t seed)
  * Copyright (c) 1990 Regents of the University of California.
  * All rights reserved.
  */
-void **git__bsearch(const void *key, void **base, size_t nmemb, int (*compar)(const void *, const void *))
+int git__bsearch(
+	void **array,
+	size_t array_len,
+	const void *key,
+	int (*compare)(const void *, const void *),
+	size_t *position)
 {
-		int lim, cmp;
-		void **p;
-
-		for (lim = nmemb; lim != 0; lim >>= 1) {
-				p = base + (lim >> 1);
-				cmp = (*compar)(key, *p);
-				if (cmp > 0) { /* key > p: move right */
-						base = p + 1;
-						lim--;
-				} else if (cmp == 0) {
-						return (void **)p;
-				} /* else move left */
-		}
-		return NULL;
+	int lim, cmp;
+	void **part, **base = array;
+
+	for (lim = array_len; lim != 0; lim >>= 1) {
+		part = base + (lim >> 1);
+		cmp = (*compare)(key, *part);
+		if (cmp == 0) {
+			*position = (part - array);
+			return GIT_SUCCESS;
+		} else if (cmp > 0) { /* key > p; take right partition */
+			base = part + 1;
+			lim--;
+		} /* else take left partition */
+	}
+
+	*position = (base - array);
+	return GIT_ENOTFOUND;
 }
 
 /**
diff --git a/src/util.h b/src/util.h
index 4b1104b..2367bb5 100644
--- a/src/util.h
+++ b/src/util.h
@@ -105,8 +105,13 @@ extern void git__strtolower(char *str);
 extern int git__fnmatch(const char *pattern, const char *name, int flags);
 
 extern void git__tsort(void **dst, size_t size, int (*cmp)(const void *, const void *));
-extern void **git__bsearch(const void *key, void **base, size_t nmemb,
-	int (*compar)(const void *, const void *));
+
+extern int git__bsearch(
+	void **array,
+	size_t array_len,
+	const void *key,
+	int (*compare)(const void *, const void *),
+	size_t *position);
 
 extern int git__strcmp_cb(const void *a, const void *b);
 
diff --git a/src/vector.c b/src/vector.c
index e745d77..593d037 100644
--- a/src/vector.c
+++ b/src/vector.c
@@ -74,6 +74,45 @@ int git_vector_insert(git_vector *v, void *element)
 	return GIT_SUCCESS;
 }
 
+int git_vector_insert_sorted(git_vector *v, void *element, int (*on_dup)(void **old, void *new))
+{
+	int error = GIT_SUCCESS;
+	size_t pos;
+
+	assert(v && v->_cmp);
+
+	if (!v->sorted)
+		git_vector_sort(v);
+
+	if (v->length >= v->_alloc_size) {
+		if (resize_vector(v) < 0)
+			return GIT_ENOMEM;
+	}
+
+	error = git__bsearch(v->contents, v->length, element, v->_cmp, &pos);
+
+	/* If we found the element and have a duplicate handler callback,
+	 * invoke it.  If it returns an error, then cancel insert, otherwise
+	 * proceed with normal insert.
+	 */
+	if (error == GIT_SUCCESS && on_dup != NULL) {
+		error = on_dup(&v->contents[pos], element);
+		if (error != GIT_SUCCESS)
+			return error;
+	}
+
+	/* shift elements to the right */
+	if (pos < v->length) {
+		memmove(v->contents + pos + 1, v->contents + pos,
+		        (v->length - pos) * sizeof(void *));
+	}
+
+	v->contents[pos] = element;
+	v->length++;
+
+	return GIT_SUCCESS;
+}
+
 void git_vector_sort(git_vector *v)
 {
 	assert(v);
@@ -87,7 +126,7 @@ void git_vector_sort(git_vector *v)
 
 int git_vector_bsearch2(git_vector *v, git_vector_cmp key_lookup, const void *key)
 {
-	void **find;
+	size_t pos;
 
 	assert(v && key && key_lookup);
 
@@ -97,9 +136,9 @@ int git_vector_bsearch2(git_vector *v, git_vector_cmp key_lookup, const void *ke
 
 	git_vector_sort(v);
 
-	find = git__bsearch(key, v->contents, v->length, key_lookup);
-	if (find != NULL)
-		return (int)(find - v->contents);
+	if (git__bsearch(v->contents, v->length, key, key_lookup,
+			&pos) == GIT_SUCCESS)
+		return (int)pos;
 
 	return git__throw(GIT_ENOTFOUND, "Can't find element");
 }
diff --git a/src/vector.h b/src/vector.h
index 4c053e6..9ee3c9e 100644
--- a/src/vector.h
+++ b/src/vector.h
@@ -45,6 +45,9 @@ GIT_INLINE(void *) git_vector_get(git_vector *v, unsigned int position)
 	for ((iter) = (v)->length; (iter) > 0 && ((elem) = (v)->contents[(iter)-1], 1); (iter)-- )
 
 int git_vector_insert(git_vector *v, void *element);
+int git_vector_insert_sorted(git_vector *v, void *element,
+	int (*on_dup)(void **old, void *new));
 int git_vector_remove(git_vector *v, unsigned int idx);
 void git_vector_uniq(git_vector *v);
+
 #endif
diff --git a/tests-clay/attr/repo.c b/tests-clay/attr/repo.c
index e80e24d..f87e7bf 100644
--- a/tests-clay/attr/repo.c
+++ b/tests-clay/attr/repo.c
@@ -40,8 +40,11 @@ void test_attr_repo__get_one(void)
 		{ "root_test2", "repoattr", GIT_ATTR_TRUE },
 		{ "root_test2", "rootattr", GIT_ATTR_FALSE },
 		{ "root_test2", "missingattr", NULL },
+		{ "root_test2", "multiattr", GIT_ATTR_FALSE },
 		{ "root_test3", "repoattr", GIT_ATTR_TRUE },
 		{ "root_test3", "rootattr", NULL },
+		{ "root_test3", "multiattr", "3" },
+		{ "root_test3", "multi2", NULL },
 		{ "subdir/subdir_test1", "repoattr", GIT_ATTR_TRUE },
 		{ "subdir/subdir_test1", "rootattr", GIT_ATTR_TRUE },
 		{ "subdir/subdir_test1", "missingattr", NULL },
@@ -166,21 +169,68 @@ void test_attr_repo__macros(void)
 {
 	const char *names[5] = { "rootattr", "binary", "diff", "crlf", "frotz" };
 	const char *names2[5] = { "mymacro", "positive", "negative", "rootattr", "another" };
+	const char *names3[3] = { "macro2", "multi2", "multi3" };
 	const char *values[5];
 
 	cl_git_pass(git_attr_get_many(g_repo, "binfile", 5, names, values));
 
 	cl_assert(values[0] == GIT_ATTR_TRUE);
-	cl_assert(values[1] == NULL);
+	cl_assert(values[1] == GIT_ATTR_TRUE);
 	cl_assert(values[2] == GIT_ATTR_FALSE);
 	cl_assert(values[3] == GIT_ATTR_FALSE);
 	cl_assert(values[4] == NULL);
 
 	cl_git_pass(git_attr_get_many(g_repo, "macro_test", 5, names2, values));
 
-	cl_assert(values[0] == NULL);
+	cl_assert(values[0] == GIT_ATTR_TRUE);
 	cl_assert(values[1] == GIT_ATTR_TRUE);
 	cl_assert(values[2] == GIT_ATTR_FALSE);
 	cl_assert(values[3] == NULL);
 	cl_assert_strequal("77", values[4]);
+
+	cl_git_pass(git_attr_get_many(g_repo, "macro_test", 3, names3, values));
+
+	cl_assert(values[0] == GIT_ATTR_TRUE);
+	cl_assert(values[1] == GIT_ATTR_FALSE);
+	cl_assert_strequal("answer", values[2]);
+}
+
+void test_attr_repo__bad_macros(void)
+{
+	const char *names[6] = { "rootattr", "positive", "negative",
+		"firstmacro", "secondmacro", "thirdmacro" };
+	const char *values[6];
+
+	cl_git_pass(git_attr_get_many(g_repo, "macro_bad", 6, names, values));
+
+	/* these three just confirm that the "mymacro" rule ran */
+	cl_assert(values[0] == NULL);
+	cl_assert(values[1] == GIT_ATTR_TRUE);
+	cl_assert(values[2] == GIT_ATTR_FALSE);
+
+	/* file contains:
+	 *     # let's try some malicious macro defs
+	 *     [attr]firstmacro -thirdmacro -secondmacro
+	 *     [attr]secondmacro firstmacro -firstmacro
+	 *     [attr]thirdmacro secondmacro=hahaha -firstmacro
+	 *     macro_bad firstmacro secondmacro thirdmacro
+	 *
+	 * firstmacro assignment list ends up with:
+	 *     -thirdmacro -secondmacro
+	 * secondmacro assignment list expands "firstmacro" and ends up with:
+	 *     -thirdmacro -secondmacro -firstmacro
+	 * thirdmacro assignment don't expand so list ends up with:
+	 *     secondmacro="hahaha"
+	 *
+	 * macro_bad assignment list ends up with:
+	 *     -thirdmacro -secondmacro firstmacro &&
+	 *     -thirdmacro -secondmacro -firstmacro secondmacro &&
+	 *     secondmacro="hahaha" thirdmacro
+	 *
+	 * so summary results should be:
+	 *     -firstmacro secondmacro="hahaha" thirdmacro
+	 */
+	cl_assert(values[3] == GIT_ATTR_FALSE);
+	cl_assert_strequal("hahaha", values[4]);
+	cl_assert(values[5] == GIT_ATTR_TRUE);
 }
diff --git a/tests-clay/clay.h b/tests-clay/clay.h
index 4a57926..e4a4135 100644
--- a/tests-clay/clay.h
+++ b/tests-clay/clay.h
@@ -68,6 +68,7 @@ extern void test_attr_lookup__check_attr_examples(void);
 extern void test_attr_lookup__from_buffer(void);
 extern void test_attr_lookup__match_variants(void);
 extern void test_attr_lookup__simple(void);
+extern void test_attr_repo__bad_macros(void);
 extern void test_attr_repo__cleanup(void);
 extern void test_attr_repo__foreach(void);
 extern void test_attr_repo__get_many(void);
@@ -141,6 +142,9 @@ extern void test_core_strtol__int64(void);
 extern void test_core_vector__0(void);
 extern void test_core_vector__1(void);
 extern void test_core_vector__2(void);
+extern void test_core_vector__3(void);
+extern void test_core_vector__4(void);
+extern void test_core_vector__5(void);
 extern void test_index_rename__single_file(void);
 extern void test_network_remotes__cleanup(void);
 extern void test_network_remotes__fnmatch(void);
diff --git a/tests-clay/clay_main.c b/tests-clay/clay_main.c
index ce881e4..a3dce81 100644
--- a/tests-clay/clay_main.c
+++ b/tests-clay/clay_main.c
@@ -122,7 +122,8 @@ static const struct clay_func _clay_cb_attr_lookup[] = {
 	{"simple", &test_attr_lookup__simple}
 };
 static const struct clay_func _clay_cb_attr_repo[] = {
-    {"foreach", &test_attr_repo__foreach},
+    {"bad_macros", &test_attr_repo__bad_macros},
+	{"foreach", &test_attr_repo__foreach},
 	{"get_many", &test_attr_repo__get_many},
 	{"get_one", &test_attr_repo__get_one},
 	{"macros", &test_attr_repo__macros},
@@ -214,7 +215,10 @@ static const struct clay_func _clay_cb_core_strtol[] = {
 static const struct clay_func _clay_cb_core_vector[] = {
     {"0", &test_core_vector__0},
 	{"1", &test_core_vector__1},
-	{"2", &test_core_vector__2}
+	{"2", &test_core_vector__2},
+	{"3", &test_core_vector__3},
+	{"4", &test_core_vector__4},
+	{"5", &test_core_vector__5}
 };
 static const struct clay_func _clay_cb_index_rename[] = {
     {"single_file", &test_index_rename__single_file}
@@ -338,7 +342,7 @@ static const struct clay_suite _clay_suites[] = {
         "attr::repo",
         {"initialize", &test_attr_repo__initialize},
         {"cleanup", &test_attr_repo__cleanup},
-        _clay_cb_attr_repo, 5
+        _clay_cb_attr_repo, 6
     },
 	{
         "buf::basic",
@@ -428,7 +432,7 @@ static const struct clay_suite _clay_suites[] = {
         "core::vector",
         {NULL, NULL},
         {NULL, NULL},
-        _clay_cb_core_vector, 3
+        _clay_cb_core_vector, 6
     },
 	{
         "index::rename",
@@ -559,7 +563,7 @@ static const struct clay_suite _clay_suites[] = {
 };
 
 static size_t _clay_suite_count = 39;
-static size_t _clay_callback_count = 134;
+static size_t _clay_callback_count = 138;
 
 /* Core test functions */
 static void
diff --git a/tests-clay/core/vector.c b/tests-clay/core/vector.c
index b8a853c..fdcfb3a 100644
--- a/tests-clay/core/vector.c
+++ b/tests-clay/core/vector.c
@@ -64,3 +64,128 @@ void test_core_vector__2(void)
 }
 
 
+static int compare_them(const void *a, const void *b)
+{
+	return (int)((long)a - (long)b);
+}
+
+/* insert_sorted */
+void test_core_vector__3(void)
+{
+	git_vector x;
+	long i;
+	git_vector_init(&x, 1, &compare_them);
+
+	for (i = 0; i < 10; i += 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	for (i = 9; i > 0; i -= 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	cl_assert(x.length == 10);
+	for (i = 0; i < 10; ++i) {
+		cl_assert(git_vector_get(&x, i) == (void*)(i + 1));
+	}
+
+	git_vector_free(&x);
+}
+
+/* insert_sorted with duplicates */
+void test_core_vector__4(void)
+{
+	git_vector x;
+	long i;
+	git_vector_init(&x, 1, &compare_them);
+
+	for (i = 0; i < 10; i += 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	for (i = 9; i > 0; i -= 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	for (i = 0; i < 10; i += 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	for (i = 9; i > 0; i -= 2) {
+		git_vector_insert_sorted(&x, (void*)(i + 1), NULL);
+	}
+
+	cl_assert(x.length == 20);
+	for (i = 0; i < 20; ++i) {
+		cl_assert(git_vector_get(&x, i) == (void*)(i / 2 + 1));
+	}
+
+	git_vector_free(&x);
+}
+
+typedef struct {
+	int content;
+	int count;
+} my_struct;
+
+static int _struct_count = 0;
+
+static int compare_structs(const void *a, const void *b)
+{
+	return ((const my_struct *)a)->content -
+		((const my_struct *)b)->content;
+}
+
+static int merge_structs(void **old_raw, void *new)
+{
+	my_struct *old = *(my_struct **)old_raw;
+	cl_assert(((my_struct *)old)->content == ((my_struct *)new)->content);
+	((my_struct *)old)->count += 1;
+	git__free(new);
+	_struct_count--;
+	return GIT_EEXISTS;
+}
+
+static my_struct *alloc_struct(int value)
+{
+	my_struct *st = git__malloc(sizeof(my_struct));
+	st->content = value;
+	st->count = 0;
+	_struct_count++;
+	return st;
+}
+
+/* insert_sorted with duplicates and special handling */
+void test_core_vector__5(void)
+{
+	git_vector x;
+	int i;
+
+	git_vector_init(&x, 1, &compare_structs);
+
+	for (i = 0; i < 10; i += 2)
+		git_vector_insert_sorted(&x, alloc_struct(i), &merge_structs);
+
+	for (i = 9; i > 0; i -= 2)
+		git_vector_insert_sorted(&x, alloc_struct(i), &merge_structs);
+
+	cl_assert(x.length == 10);
+	cl_assert(_struct_count == 10);
+
+	for (i = 0; i < 10; i += 2)
+		git_vector_insert_sorted(&x, alloc_struct(i), &merge_structs);
+
+	for (i = 9; i > 0; i -= 2)
+		git_vector_insert_sorted(&x, alloc_struct(i), &merge_structs);
+
+	cl_assert(x.length == 10);
+	cl_assert(_struct_count == 10);
+
+	for (i = 0; i < 10; ++i) {
+		cl_assert(((my_struct *)git_vector_get(&x, i))->content == i);
+		git__free(git_vector_get(&x, i));
+		_struct_count--;
+	}
+
+	git_vector_free(&x);
+}
diff --git a/tests/resources/attr/.gitted/index b/tests/resources/attr/.gitted/index
index 9c59073..c52747e 100644
Binary files a/tests/resources/attr/.gitted/index and b/tests/resources/attr/.gitted/index differ
diff --git a/tests/resources/attr/.gitted/logs/HEAD b/tests/resources/attr/.gitted/logs/HEAD
index 3c40451..f518a46 100644
--- a/tests/resources/attr/.gitted/logs/HEAD
+++ b/tests/resources/attr/.gitted/logs/HEAD
@@ -1,2 +1,3 @@
 0000000000000000000000000000000000000000 6bab5c79cd5140d0f800917f550eb2a3dc32b0da Russell Belfer <arrbee@arrbee.com> 1324416995 -0800	commit (initial): initial test data
 6bab5c79cd5140d0f800917f550eb2a3dc32b0da 605812ab7fe421fdd325a935d35cb06a9234a7d7 Russell Belfer <arrbee@arrbee.com> 1325143098 -0800	commit: latest test updates
+605812ab7fe421fdd325a935d35cb06a9234a7d7 a5d76cad53f66f1312bd995909a5bab3c0820770 Russell Belfer <arrbee@arrbee.com> 1325281762 -0800	commit: more macro tests
diff --git a/tests/resources/attr/.gitted/logs/refs/heads/master b/tests/resources/attr/.gitted/logs/refs/heads/master
index 3c40451..f518a46 100644
--- a/tests/resources/attr/.gitted/logs/refs/heads/master
+++ b/tests/resources/attr/.gitted/logs/refs/heads/master
@@ -1,2 +1,3 @@
 0000000000000000000000000000000000000000 6bab5c79cd5140d0f800917f550eb2a3dc32b0da Russell Belfer <arrbee@arrbee.com> 1324416995 -0800	commit (initial): initial test data
 6bab5c79cd5140d0f800917f550eb2a3dc32b0da 605812ab7fe421fdd325a935d35cb06a9234a7d7 Russell Belfer <arrbee@arrbee.com> 1325143098 -0800	commit: latest test updates
+605812ab7fe421fdd325a935d35cb06a9234a7d7 a5d76cad53f66f1312bd995909a5bab3c0820770 Russell Belfer <arrbee@arrbee.com> 1325281762 -0800	commit: more macro tests
diff --git a/tests/resources/attr/.gitted/objects/2b/40c5aca159b04ea8d20ffe36cdf8b09369b14a b/tests/resources/attr/.gitted/objects/2b/40c5aca159b04ea8d20ffe36cdf8b09369b14a
new file mode 100644
index 0000000..0e23680
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/2b/40c5aca159b04ea8d20ffe36cdf8b09369b14a
@@ -0,0 +1 @@
+xmPj0=P8ZSchR6{=ob"afv#3ά=7P%[8<He`&]@?aFZ@!.:ldLG|K7~XN8Id}q2cG7l5V_pE#lZGMt[J½&hu][4-3;Cg4x`ZYÌ錻b^>yNlͣ>c;gӐkYX9b|D~Vؗ)vܕ
\ No newline at end of file
diff --git a/tests/resources/attr/.gitted/objects/58/19a185d77b03325aaf87cafc771db36f6ddca7 b/tests/resources/attr/.gitted/objects/58/19a185d77b03325aaf87cafc771db36f6ddca7
new file mode 100644
index 0000000..fe34eb6
Binary files /dev/null and b/tests/resources/attr/.gitted/objects/58/19a185d77b03325aaf87cafc771db36f6ddca7 differ
diff --git a/tests/resources/attr/.gitted/objects/a5/d76cad53f66f1312bd995909a5bab3c0820770 b/tests/resources/attr/.gitted/objects/a5/d76cad53f66f1312bd995909a5bab3c0820770
new file mode 100644
index 0000000..cd6a389
--- /dev/null
+++ b/tests/resources/attr/.gitted/objects/a5/d76cad53f66f1312bd995909a5bab3c0820770
@@ -0,0 +1,4 @@
+x]
+!E{vB>!"ZB;u3Cm	{.7Z4avfgBLEeP;NQڬBLAnŲI 5I)M6ZQ[
+h3e:
+
}u};|)z&pbq?3TJ13JX
\ No newline at end of file
diff --git a/tests/resources/attr/.gitted/objects/d5/7da33c16b14326ecb05d19bbea908f5e4c47d9 b/tests/resources/attr/.gitted/objects/d5/7da33c16b14326ecb05d19bbea908f5e4c47d9
new file mode 100644
index 0000000..b96d40c
Binary files /dev/null and b/tests/resources/attr/.gitted/objects/d5/7da33c16b14326ecb05d19bbea908f5e4c47d9 differ
diff --git a/tests/resources/attr/.gitted/refs/heads/master b/tests/resources/attr/.gitted/refs/heads/master
index 1049fe4..0516af2 100644
--- a/tests/resources/attr/.gitted/refs/heads/master
+++ b/tests/resources/attr/.gitted/refs/heads/master
@@ -1 +1 @@
-605812ab7fe421fdd325a935d35cb06a9234a7d7
+a5d76cad53f66f1312bd995909a5bab3c0820770
diff --git a/tests/resources/attr/gitattributes b/tests/resources/attr/gitattributes
index 94da4fa..2b40c5a 100644
--- a/tests/resources/attr/gitattributes
+++ b/tests/resources/attr/gitattributes
@@ -3,5 +3,22 @@ root_test2 -rootattr
 root_test3 !rootattr
 binfile	   		   binary
 abc		foo bar baz
+
+root_test2 multiattr
+root_test3 multi2=foo
+
+root_test3 multiattr=1 multiattr=2 multiattr=3 multi2=abc !multi2
+root_test2 multiattr=string -multiattr
+
 [attr]mymacro positive -negative !rootattr
 macro* mymacro another=77
+
+[attr]macro2 multi2 -multi2 multi3 !multi3 multi3=answer
+macro* macro2 macro2 macro2
+
+# let's try some malicious macro defs
+[attr]firstmacro -thirdmacro -secondmacro
+[attr]secondmacro firstmacro -firstmacro
+[attr]thirdmacro secondmacro=hahaha
+
+macro_bad firstmacro secondmacro thirdmacro
diff --git a/tests/resources/attr/macro_bad b/tests/resources/attr/macro_bad
new file mode 100644
index 0000000..5819a18
--- /dev/null
+++ b/tests/resources/attr/macro_bad
@@ -0,0 +1 @@
+boo