Commit 0d64bef941928046d114c4da1acb70bd2907855e

Russell Belfer 2012-10-05T15:56:57

Add complex checkout test and then fix checkout This started as a complex new test for checkout going through the "typechanges" test repository, but that revealed numerous issues with checkout, including: * complete failure with submodules * failure to create blobs with exec bits * problems when replacing a tree with a blob because the tree "example/" sorts after the blob "example" so the delete was being processed after the single file blob was created This fixes most of those problems and includes a number of other minor changes that made it easier to do that, including improving the TYPECHANGE support in diff/status, etc.

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
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
diff --git a/include/git2/checkout.h b/include/git2/checkout.h
index ef3badb..fb1a230 100644
--- a/include/git2/checkout.h
+++ b/include/git2/checkout.h
@@ -21,12 +21,27 @@
  */
 GIT_BEGIN_DECL
 
-enum {
+/**
+ * Checkout behavior flags
+ *
+ * These flags control what checkout does with files.  Pass in a
+ * combination of these values OR'ed together.
+ *
+ * - GIT_CHECKOUT_DEFAULT: With this value, checkout does not update
+ *   any files in the working directory.
+ * - GIT_CHECKOUT_OVERWRITE_MODIFIED: When a file exists and is modified,
+ *   replace the modifications with the new version.
+ * - GIT_CHECKOUT_CREATE_MISSING: When a file does not exist in the
+ *   working directory, create it.
+ * - GIT_CHECKOUT_REMOVE_UNTRACKED: If an untracked file in encountered
+ *   in the working directory, delete it.
+ */
+typedef enum {
 	GIT_CHECKOUT_DEFAULT			= (1 << 0),
 	GIT_CHECKOUT_OVERWRITE_MODIFIED	= (1 << 1),
 	GIT_CHECKOUT_CREATE_MISSING		= (1 << 2),
 	GIT_CHECKOUT_REMOVE_UNTRACKED	= (1 << 3),
-};
+} git_checkout_strategy_t;
 
 /* Use zeros to indicate default settings */
 typedef struct git_checkout_opts {
diff --git a/include/git2/diff.h b/include/git2/diff.h
index 551e525..1d32d9a 100644
--- a/include/git2/diff.h
+++ b/include/git2/diff.h
@@ -56,6 +56,9 @@ GIT_BEGIN_DECL
  * - GIT_DIFF_DONT_SPLIT_TYPECHANGE: normally, a type change between files
  *   will be converted into a DELETED record for the old file and an ADDED
  *   record for the new one; this option enabled TYPECHANGE records.
+ * - GIT_DIFF_SKIP_BINARY_CHECK: the binary flag in the delta record will
+ *   not be updated.  This is useful if iterating over a diff without hunk
+ *   and line callbacks and you want to avoid loading files completely.
  */
 enum {
 	GIT_DIFF_NORMAL = 0,
@@ -73,7 +76,9 @@ enum {
 	GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 11),
 	GIT_DIFF_DELTAS_ARE_ICASE = (1 << 12),
 	GIT_DIFF_INCLUDE_UNTRACKED_CONTENT = (1 << 13),
-	GIT_DIFF_DONT_SPLIT_TYPECHANGE = (1 << 14),
+	GIT_DIFF_SKIP_BINARY_CHECK = (1 << 14),
+	GIT_DIFF_INCLUDE_TYPECHANGE = (1 << 15),
+	GIT_DIFF_INCLUDE_TYPECHANGE_TREES = (1 << 16),
 };
 
 /**
diff --git a/include/git2/tree.h b/include/git2/tree.h
index e526141..2ee1f4a 100644
--- a/include/git2/tree.h
+++ b/include/git2/tree.h
@@ -100,7 +100,7 @@ GIT_EXTERN(git_tree_entry *) git_tree_entry_dup(const git_tree_entry *entry);
  * @param tree a previously loaded tree.
  * @return object identity for the tree.
  */
-GIT_EXTERN(const git_oid *) git_tree_id(git_tree *tree);
+GIT_EXTERN(const git_oid *) git_tree_id(const git_tree *tree);
 
 /**
  * Get the number of entries listed in a tree
@@ -108,7 +108,7 @@ GIT_EXTERN(const git_oid *) git_tree_id(git_tree *tree);
  * @param tree a previously loaded tree.
  * @return the number of entries in the tree
  */
-GIT_EXTERN(unsigned int) git_tree_entrycount(git_tree *tree);
+GIT_EXTERN(unsigned int) git_tree_entrycount(const git_tree *tree);
 
 /**
  * Lookup a tree entry by its filename
diff --git a/src/checkout.c b/src/checkout.c
index ee6e043..6f5cfff 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -30,6 +30,7 @@ struct checkout_diff_data
 	git_indexer_stats *stats;
 	git_repository *owner;
 	bool can_symlink;
+	bool create_submodules;
 	int error;
 };
 
@@ -40,18 +41,27 @@ static int buffer_to_file(
 	int file_open_flags,
 	mode_t file_mode)
 {
-	int fd, error_write, error_close;
+	int fd, error, error_close;
 
-	if (git_futils_mkpath2file(path, dir_mode) < 0)
-		return -1;
+	if ((error = git_futils_mkpath2file(path, dir_mode)) < 0)
+		return error;
 
 	if ((fd = p_open(path, file_open_flags, file_mode)) < 0)
-		return -1;
+		return fd;
+
+	error = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer));
 
-	error_write = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer));
 	error_close = p_close(fd);
 
-	return error_write ? error_write : error_close;
+	if (!error)
+		error = error_close;
+
+	if (!error &&
+		(file_mode & 0100) != 0 &&
+		(error = p_chmod(path, file_mode)) < 0)
+		giterr_set(GITERR_OS, "Failed to set permissions on '%s'", path);
+
+	return error;
 }
 
 static int blob_content_to_file(
@@ -125,107 +135,122 @@ static int blob_content_to_link(git_blob *blob, const char *path, bool can_symli
 	return error;
 }
 
+static int checkout_submodule(
+	struct checkout_diff_data *data,
+	const git_diff_file *file)
+{
+	if (git_futils_mkdir(
+			file->path, git_repository_workdir(data->owner),
+			data->checkout_opts->dir_mode, GIT_MKDIR_PATH) < 0)
+		return -1;
+
+	/* TODO: two cases:
+	 * 1 - submodule already checked out, but we need to move the HEAD
+	 *     to the new OID, or
+	 * 2 - submodule not checked out and we should recursively check it out
+	 *
+	 * Checkout will not execute a pull request on the submodule, but a
+	 * clone command should probably be able to.  Do we need a submodule
+	 * callback option?
+	 */
+
+	return 0;
+}
+
 static int checkout_blob(
-	git_repository *repo,
-	const git_oid *blob_oid,
-	const char *path,
-	mode_t filemode,
-	bool can_symlink,
-	git_checkout_opts *opts)
+	struct checkout_diff_data *data,
+	const git_diff_file *file)
 {
 	git_blob *blob;
 	int error;
 
-	if ((error = git_blob_lookup(&blob, repo, blob_oid)) < 0)
-		return error; /* Add an error message */
+	git_buf_truncate(data->path, data->workdir_len);
+	if (git_buf_joinpath(data->path, git_buf_cstr(data->path), file->path) < 0)
+		return -1;
+
+	if ((error = git_blob_lookup(&blob, data->owner, &file->oid)) < 0)
+		return error;
 
-	if (S_ISLNK(filemode))
-		error = blob_content_to_link(blob, path, can_symlink);
+	if (S_ISLNK(file->mode))
+		error = blob_content_to_link(
+			blob, git_buf_cstr(data->path), data->can_symlink);
 	else
-		error = blob_content_to_file(blob, path, filemode, opts);
+		error = blob_content_to_file(
+			blob, git_buf_cstr(data->path), file->mode, data->checkout_opts);
 
 	git_blob_free(blob);
 
 	return error;
 }
 
-static int checkout_diff_fn(
-	void *cb_data,
-	const git_diff_delta *delta,
-	float progress)
+static int checkout_remove_the_old(
+	void *cb_data, const git_diff_delta *delta, float progress)
 {
 	struct checkout_diff_data *data = cb_data;
-	int error = 0;
 	git_checkout_opts *opts = data->checkout_opts;
-	bool do_delete = false, do_checkout_blob = false;
-
-	data->stats->processed = (unsigned int)(data->stats->total * progress);
-
-	switch (delta->status) {
-	case GIT_DELTA_UNTRACKED:
-		if ((opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0)
-			do_delete = true;
-		break;
-
-	case GIT_DELTA_MODIFIED:
-	case GIT_DELTA_TYPECHANGE:
-		if (!(opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED)) {
-
-			if (opts->skipped_notify_cb != NULL &&
-				opts->skipped_notify_cb(
-					delta->new_file.path,
-					&delta->old_file.oid,
-					delta->old_file.mode,
-					opts->notify_payload) != 0)
-			{
-				giterr_clear();
-				error = GIT_EUSER;
-			}
-
-			goto cleanup;
-		}
-
-		do_checkout_blob = true;
 
-		if (delta->status == GIT_DELTA_TYPECHANGE)
-			do_delete = true;
-		break;
+	GIT_UNUSED(progress);
+	data->stats->processed++;
+
+	if ((delta->status == GIT_DELTA_UNTRACKED &&
+		 (opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0) ||
+		(delta->status == GIT_DELTA_TYPECHANGE &&
+		 (opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0))
+	{
+		data->error = git_futils_rmdir_r(
+			delta->new_file.path,
+			git_repository_workdir(data->owner),
+			GIT_DIRREMOVAL_FILES_AND_DIRS);
+	}
 
-	case GIT_DELTA_DELETED:
-		if ((opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING) != 0)
-			do_checkout_blob = true;
-		break;
+	return data->error;
+}
 
-	default:
-		giterr_set(GITERR_INVALID, "Unexpected status (%d) for path '%s'.",
-			delta->status, delta->new_file.path);
-		error = -1;
-		goto cleanup;
+static int checkout_create_the_new(
+	void *cb_data, const git_diff_delta *delta, float progress)
+{
+	int error = 0;
+	struct checkout_diff_data *data = cb_data;
+	git_checkout_opts *opts = data->checkout_opts;
+	bool do_checkout = false, do_notify = false;
+
+	GIT_UNUSED(progress);
+	data->stats->processed++;
+
+	if (delta->status == GIT_DELTA_MODIFIED ||
+		delta->status == GIT_DELTA_TYPECHANGE)
+	{
+		if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0)
+			do_checkout = true;
+		else if (opts->skipped_notify_cb != NULL)
+			do_notify = !data->create_submodules;
+	}
+	else if (delta->status == GIT_DELTA_DELETED &&
+			 (opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING) != 0)
+		do_checkout = true;
+
+	if (do_notify) {
+		if (opts->skipped_notify_cb(
+			delta->old_file.path, &delta->old_file.oid,
+			delta->old_file.mode, opts->notify_payload))
+		{
+			giterr_clear();
+			error = GIT_EUSER;
+		}
 	}
 
-	git_buf_truncate(data->path, data->workdir_len);
-
-	if ((error = git_buf_joinpath(
-			data->path, git_buf_cstr(data->path), delta->new_file.path)) < 0)
-		goto cleanup;
+	if (do_checkout) {
+		bool is_submodule = S_ISGITLINK(delta->old_file.mode);
 
-	if (do_delete &&
-		(error = git_futils_rmdir_r(
-			git_buf_cstr(data->path), GIT_DIRREMOVAL_FILES_AND_DIRS)) < 0)
-		goto cleanup;
+		if (!is_submodule && !data->create_submodules)
+			error = checkout_blob(data, &delta->old_file);
 
-	if (do_checkout_blob)
-		error = checkout_blob(
-			data->owner,
-			&delta->old_file.oid,
-			git_buf_cstr(data->path),
-			delta->old_file.mode,
-			data->can_symlink,
-			opts);
+		else if (is_submodule && data->create_submodules)
+			error = checkout_submodule(data, &delta->old_file);
+	}
 
-cleanup:
 	if (error)
-		data->error = error; /* preserve real error */
+		data->error = error;
 
 	return error;
 }
@@ -278,7 +303,6 @@ int git_checkout_index(
 	git_checkout_opts *opts,
 	git_indexer_stats *stats)
 {
-	git_index *index = NULL;
 	git_diff_list *diff = NULL;
 	git_indexer_stats dummy_stats;
 
@@ -292,11 +316,13 @@ int git_checkout_index(
 
 	assert(repo);
 
-	if ((git_repository__ensure_not_bare(repo, "checkout")) < 0)
-		return GIT_EBAREREPO;
+	if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0)
+		return error;
 
-	diff_opts.flags = GIT_DIFF_INCLUDE_UNTRACKED |
-		GIT_DIFF_DONT_SPLIT_TYPECHANGE;
+	diff_opts.flags =
+		GIT_DIFF_INCLUDE_UNTRACKED |
+		GIT_DIFF_INCLUDE_TYPECHANGE |
+		GIT_DIFF_SKIP_BINARY_CHECK;
 
 	if (opts && opts->paths.count > 0)
 		diff_opts.pathspec = opts->paths;
@@ -313,11 +339,7 @@ int git_checkout_index(
 		stats = &dummy_stats;
 
 	stats->processed = 0;
-
-	if ((git_repository_index(&index, repo)) < 0)
-		goto cleanup;
-
-	stats->total = git_index_entrycount(index);
+	stats->total = (unsigned int)git_diff_num_deltas(diff) * 3 /* # passes */;
 
 	memset(&data, 0, sizeof(data));
 
@@ -330,15 +352,33 @@ int git_checkout_index(
 	if ((error = retrieve_symlink_capabilities(repo, &data.can_symlink)) < 0)
 		goto cleanup;
 
-	error = git_diff_foreach(diff, &data, checkout_diff_fn, NULL, NULL);
+	/* Checkout is best performed with three passes through the diff.
+	 *
+	 * 1. First do removes, because we iterate in alphabetical order, thus
+	 *    a new untracked directory will end up sorted *after* a blob that
+	 *    should be checked out with the same name.
+	 * 2. Then checkout all blobs.
+	 * 3. Then checkout all submodules in case a new .gitmodules blob was
+	 *    checked out during pass #2.
+	 */
 
+	if (!(error = git_diff_foreach(
+			diff, &data, checkout_remove_the_old, NULL, NULL)) &&
+		!(error = git_diff_foreach(
+			diff, &data, checkout_create_the_new, NULL, NULL)))
+	{
+		data.create_submodules = true;
+		error = git_diff_foreach(
+			diff, &data, checkout_create_the_new, NULL, NULL);
+	}
+
+cleanup:
 	if (error == GIT_EUSER)
 		error = (data.error != 0) ? data.error : -1;
 
-cleanup:
-	git_index_free(index);
 	git_diff_list_free(diff);
 	git_buf_free(&workdir);
+
 	return error;
 }
 
diff --git a/src/clone.c b/src/clone.c
index 00e39d3..d16d098 100644
--- a/src/clone.c
+++ b/src/clone.c
@@ -314,7 +314,7 @@ static int clone_internal(
 		if ((retcode = setup_remotes_and_fetch(repo, origin_url, fetch_stats)) < 0) {
 			/* Failed to fetch; clean up */
 			git_repository_free(repo);
-			git_futils_rmdir_r(path, GIT_DIRREMOVAL_FILES_AND_DIRS);
+			git_futils_rmdir_r(path, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS);
 		} else {
 			*out = repo;
 			retcode = 0;
diff --git a/src/diff.c b/src/diff.c
index f88bda4..7f500b8 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -291,6 +291,36 @@ static int diff_delta__from_two(
 	return 0;
 }
 
+static git_diff_delta *diff_delta__last_for_item(
+	git_diff_list *diff,
+	const git_index_entry *item)
+{
+	git_diff_delta *delta = git_vector_last(&diff->deltas);
+	if (!delta)
+		return NULL;
+
+	switch (delta->status) {
+	case GIT_DELTA_UNMODIFIED:
+	case GIT_DELTA_DELETED:
+		if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0)
+			return delta;
+		break;
+	case GIT_DELTA_ADDED:
+		if (git_oid_cmp(&delta->new_file.oid, &item->oid) == 0)
+			return delta;
+		break;
+	case GIT_DELTA_MODIFIED:
+		if (git_oid_cmp(&delta->old_file.oid, &item->oid) == 0 ||
+			git_oid_cmp(&delta->new_file.oid, &item->oid) == 0)
+			return delta;
+		break;
+	default:
+		break;
+	}
+
+	return NULL;
+}
+
 static char *diff_strdup_prefix(git_pool *pool, const char *prefix)
 {
 	size_t len = strlen(prefix);
@@ -368,6 +398,10 @@ static git_diff_list *git_diff_list_alloc(
 		diff->opts.new_prefix = swap;
 	}
 
+	/* INCLUDE_TYPECHANGE_TREES implies INCLUDE_TYPECHANGE */
+	if (diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES)
+		diff->opts.flags |= GIT_DIFF_INCLUDE_TYPECHANGE;
+
 	/* only copy pathspec if it is "interesting" so we can test
 	 * diff->pathspec.length > 0 to know if it is worth calling
 	 * fnmatch as we iterate.
@@ -537,7 +571,7 @@ static int maybe_modified(
 
 	/* if basic type of file changed, then split into delete and add */
 	else if (GIT_MODE_TYPE(omode) != GIT_MODE_TYPE(nmode)) {
-		if ((diff->opts.flags & GIT_DIFF_DONT_SPLIT_TYPECHANGE) != 0)
+		if ((diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE) != 0)
 			status = GIT_DELTA_TYPECHANGE;
 		else {
 			if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 ||
@@ -590,7 +624,7 @@ static int maybe_modified(
 				/* grab OID while we are here */
 				if (git_oid_iszero(&nitem->oid)) {
 					const git_oid *sm_oid = git_submodule_wd_oid(sub);
-					if (sub != NULL) {
+					if (sm_oid != NULL) {
 						git_oid_cpy(&noid, sm_oid);
 						use_noid = &noid;
 					}
@@ -632,6 +666,24 @@ static int git_index_entry_cmp_icase(const void *a, const void *b)
 	return strcasecmp(entry_a->path, entry_b->path);
 }
 
+static bool entry_is_prefixed(
+	const git_index_entry *item,
+	git_iterator *prefix_iterator,
+	const git_index_entry *prefix_item)
+{
+	size_t pathlen;
+
+	if (!prefix_item ||
+		ITERATOR_PREFIXCMP(*prefix_iterator, prefix_item->path, item->path))
+		return false;
+
+	pathlen = strlen(item->path);
+
+	return (item->path[pathlen - 1] == '/' ||
+			prefix_item->path[pathlen] == '\0' ||
+			prefix_item->path[pathlen] == '/');
+}
+
 static int diff_from_iterators(
 	git_repository *repo,
 	const git_diff_options *opts, /**< can be NULL for defaults */
@@ -681,8 +733,24 @@ static int diff_from_iterators(
 
 		/* create DELETED records for old items not matched in new */
 		if (oitem && (!nitem || entry_compare(oitem, nitem) < 0)) {
-			if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0 ||
-				git_iterator_advance(old_iter, &oitem) < 0)
+			if (diff_delta__from_one(diff, GIT_DELTA_DELETED, oitem) < 0)
+				goto fail;
+
+			/* if we are generating TYPECHANGE records then check for that
+			 * instead of just generating a DELETE record
+			 */
+			if ((diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 &&
+				entry_is_prefixed(oitem, new_iter, nitem))
+			{
+				/* this entry has become a tree! convert to TYPECHANGE */
+				git_diff_delta *last = diff_delta__last_for_item(diff, oitem);
+				if (last) {
+					last->status = GIT_DELTA_TYPECHANGE;
+					last->new_file.mode = GIT_FILEMODE_TREE;
+				}
+			}
+
+			if (git_iterator_advance(old_iter, &oitem) < 0)
 				goto fail;
 		}
 
@@ -704,8 +772,7 @@ static int diff_from_iterators(
 				 * directories and it is not under an ignored directory.
 				 */
 				bool contains_tracked =
-					(oitem &&
-					 !ITERATOR_PREFIXCMP(*old_iter, oitem->path, nitem->path));
+					entry_is_prefixed(nitem, old_iter, oitem);
 				bool recurse_untracked =
 					(delta_type == GIT_DELTA_UNTRACKED &&
 					 (diff->opts.flags & GIT_DIFF_RECURSE_UNTRACKED_DIRS) != 0);
@@ -761,8 +828,25 @@ static int diff_from_iterators(
 			else if (new_iter->type != GIT_ITERATOR_WORKDIR)
 				delta_type = GIT_DELTA_ADDED;
 
-			if (diff_delta__from_one(diff, delta_type, nitem) < 0 ||
-				git_iterator_advance(new_iter, &nitem) < 0)
+			if (diff_delta__from_one(diff, delta_type, nitem) < 0)
+				goto fail;
+
+			/* if we are generating TYPECHANGE records then check for that
+			 * instead of just generating an ADD/UNTRACKED record
+			 */
+			if (delta_type != GIT_DELTA_IGNORED &&
+				(diff->opts.flags & GIT_DIFF_INCLUDE_TYPECHANGE_TREES) != 0 &&
+				entry_is_prefixed(nitem, old_iter, oitem))
+			{
+				/* this entry was a tree! convert to TYPECHANGE */
+				git_diff_delta *last = diff_delta__last_for_item(diff, oitem);
+				if (last) {
+					last->status = GIT_DELTA_TYPECHANGE;
+					last->old_file.mode = GIT_FILEMODE_TREE;
+				}
+			}
+
+			if (git_iterator_advance(new_iter, &nitem) < 0)
 				goto fail;
 		}
 
diff --git a/src/diff_output.c b/src/diff_output.c
index 10fbd39..5f0d13c 100644
--- a/src/diff_output.c
+++ b/src/diff_output.c
@@ -533,6 +533,11 @@ static int diff_patch_load(
 	if (delta->binary == 1)
 		goto cleanup;
 
+	if (!ctxt->hunk_cb &&
+		!ctxt->data_cb &&
+		(ctxt->opts->flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0)
+		goto cleanup;
+
 	switch (delta->status) {
 	case GIT_DELTA_ADDED:
 		delta->old_file.flags |= GIT_DIFF_FILE_NO_DATA;
@@ -698,8 +703,10 @@ static int diff_patch_generate(
 	if ((patch->flags & GIT_DIFF_PATCH_DIFFABLE) == 0)
 		return 0;
 
-	if (ctxt)
-		patch->ctxt = ctxt;
+	if (!ctxt->file_cb && !ctxt->hunk_cb)
+		return 0;
+
+	patch->ctxt = ctxt;
 
 	memset(&xdiff_callback, 0, sizeof(xdiff_callback));
 	xdiff_callback.outf = diff_patch_cb;
@@ -1360,7 +1367,9 @@ int git_diff_get_patch(
 	if (delta_ptr)
 		*delta_ptr = delta;
 
-	if (!patch_ptr && delta->binary != -1)
+	if (!patch_ptr &&
+		(delta->binary != -1 ||
+		 (diff->opts.flags & GIT_DIFF_SKIP_BINARY_CHECK) != 0))
 		return 0;
 
 	diff_context_init(
diff --git a/src/fileops.c b/src/fileops.c
index 8ccf063..23bfa8e 100644
--- a/src/fileops.c
+++ b/src/fileops.c
@@ -323,10 +323,6 @@ static int _rmdir_recurs_foreach(void *opaque, git_buf *path)
 {
 	git_directory_removal_type removal_type = *(git_directory_removal_type *)opaque;
 
-	assert(removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY
-		|| removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS
-		|| removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS);
-
 	if (git_path_isdir(path->ptr) == true) {
 		if (git_path_direach(path, _rmdir_recurs_foreach, opaque) < 0)
 			return -1;
@@ -359,15 +355,24 @@ static int _rmdir_recurs_foreach(void *opaque, git_buf *path)
 	return 0;
 }
 
-int git_futils_rmdir_r(const char *path, git_directory_removal_type removal_type)
+int git_futils_rmdir_r(
+	const char *path, const char *base, git_directory_removal_type removal_type)
 {
 	int error;
-	git_buf p = GIT_BUF_INIT;
+	git_buf fullpath = GIT_BUF_INIT;
+
+	assert(removal_type == GIT_DIRREMOVAL_EMPTY_HIERARCHY
+		|| removal_type == GIT_DIRREMOVAL_FILES_AND_DIRS
+		|| removal_type == GIT_DIRREMOVAL_ONLY_EMPTY_DIRS);
+
+	/* build path and find "root" where we should start calling mkdir */
+	if (git_path_join_unrooted(&fullpath, path, base, NULL) < 0)
+		return -1;
+
+	error = _rmdir_recurs_foreach(&removal_type, &fullpath);
+
+	git_buf_free(&fullpath);
 
-	error = git_buf_sets(&p, path);
-	if (!error)
-		error = _rmdir_recurs_foreach(&removal_type, &p);
-	git_buf_free(&p);
 	return error;
 }
 
diff --git a/src/fileops.h b/src/fileops.h
index d2944f4..19f7ffd 100644
--- a/src/fileops.h
+++ b/src/fileops.h
@@ -107,15 +107,17 @@ typedef enum {
  * Remove path and any files and directories beneath it.
  *
  * @param path Path to to top level directory to process.
- *
+ * @param base Root for relative path.
  * @param removal_type GIT_DIRREMOVAL_EMPTY_HIERARCHY to remove a hierarchy
- * of empty directories (will fail if any file is found), GIT_DIRREMOVAL_FILES_AND_DIRS
- * to remove a hierarchy of files and folders, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS to only remove
- * empty directories (no failure on file encounter).
+ *                     of empty directories (will fail if any file is found),
+ *                     GIT_DIRREMOVAL_FILES_AND_DIRS to remove a hierarchy of
+ *                     files and folders,
+ *                     GIT_DIRREMOVAL_ONLY_EMPTY_DIRS to only remove empty
+ *                     directories (no failure on file encounter).
  *
  * @return 0 on success; -1 on error.
  */
-extern int git_futils_rmdir_r(const char *path, git_directory_removal_type removal_type);
+extern int git_futils_rmdir_r(const char *path, const char *base, git_directory_removal_type removal_type);
 
 /**
  * Create and open a temporary file with a `_git2_` suffix.
diff --git a/src/iterator.c b/src/iterator.c
index 267687e..df6da9a 100644
--- a/src/iterator.c
+++ b/src/iterator.c
@@ -82,7 +82,7 @@ int git_iterator_for_nothing(git_iterator **iter)
 
 typedef struct tree_iterator_frame tree_iterator_frame;
 struct tree_iterator_frame {
-	tree_iterator_frame *next;
+	tree_iterator_frame *next, *prev;
 	git_tree *tree;
 	char *start;
 	unsigned int index;
@@ -91,7 +91,7 @@ struct tree_iterator_frame {
 typedef struct {
 	git_iterator base;
 	git_repository *repo;
-	tree_iterator_frame *stack;
+	tree_iterator_frame *stack, *tail;
 	git_index_entry entry;
 	git_buf path;
 	bool path_has_filename;
@@ -119,8 +119,10 @@ static void tree_iterator__pop_frame(tree_iterator *ti)
 {
 	tree_iterator_frame *tf = ti->stack;
 	ti->stack = tf->next;
-	if (ti->stack != NULL) /* don't free the initial tree */
-		git_tree_free(tf->tree);
+	if (ti->stack != NULL) {
+		git_tree_free(tf->tree); /* don't free the initial tree */
+		ti->stack->prev = NULL;  /* disconnect prev */
+	}
 	git__free(tf);
 }
 
@@ -221,6 +223,7 @@ static int tree_iterator__expand_tree(tree_iterator *ti)
 
 		tf->next  = ti->stack;
 		ti->stack = tf;
+		tf->next->prev = tf;
 
 		te = tree_iterator__tree_entry(ti);
 	}
@@ -312,7 +315,7 @@ int git_iterator_for_tree_range(
 	ITERATOR_BASE_INIT(ti, tree, TREE);
 
 	ti->repo  = repo;
-	ti->stack = tree_iterator__alloc_frame(tree, ti->base.start);
+	ti->stack = ti->tail = tree_iterator__alloc_frame(tree, ti->base.start);
 
 	if ((error = tree_iterator__expand_tree(ti)) < 0)
 		git_iterator_free((git_iterator *)ti);
@@ -864,6 +867,45 @@ int git_iterator_current_tree_entry(
 	return 0;
 }
 
+int git_iterator_current_parent_tree(
+	git_iterator *iter,
+	const char *parent_path,
+	const git_tree **tree_ptr)
+{
+	tree_iterator *ti = (tree_iterator *)iter;
+	tree_iterator_frame *tf;
+	const char *scan = parent_path;
+
+	if (iter->type != GIT_ITERATOR_TREE || ti->stack == NULL)
+		goto notfound;
+
+	for (tf = ti->tail; tf != NULL; tf = tf->prev) {
+		const git_tree_entry *te;
+
+		if (!*scan) {
+			*tree_ptr = tf->tree;
+			return 0;
+		}
+
+		te = git_tree_entry_byindex(tf->tree, tf->index);
+
+		if (strncmp(scan, te->filename, te->filename_len) != 0)
+			goto notfound;
+
+		scan += te->filename_len;
+
+		if (*scan) {
+			if (*scan != '/')
+				goto notfound;
+			scan++;
+		}
+	}
+
+notfound:
+	*tree_ptr = NULL;
+	return 0;
+}
+
 int git_iterator_current_is_ignored(git_iterator *iter)
 {
 	return (iter->type != GIT_ITERATOR_WORKDIR) ? 0 :
diff --git a/src/iterator.h b/src/iterator.h
index 29c8985..d7df501 100644
--- a/src/iterator.h
+++ b/src/iterator.h
@@ -142,6 +142,9 @@ GIT_INLINE(git_iterator_type_t) git_iterator_type(git_iterator *iter)
 extern int git_iterator_current_tree_entry(
 	git_iterator *iter, const git_tree_entry **tree_entry);
 
+extern int git_iterator_current_parent_tree(
+	git_iterator *iter, const char *parent_path, const git_tree **tree_ptr);
+
 extern int git_iterator_current_is_ignored(git_iterator *iter);
 
 /**
diff --git a/src/reflog.c b/src/reflog.c
index 80e40b9..a1ea7a2 100644
--- a/src/reflog.c
+++ b/src/reflog.c
@@ -372,7 +372,7 @@ int git_reflog_rename(git_reference *ref, const char *new_name)
 		goto cleanup;
 
 	if (git_path_isdir(git_buf_cstr(&new_path)) && 
-		(git_futils_rmdir_r(git_buf_cstr(&new_path), GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0))
+		(git_futils_rmdir_r(git_buf_cstr(&new_path), NULL, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0))
 		goto cleanup;
 
 	if (git_futils_mkpath2file(git_buf_cstr(&new_path), GIT_REFLOG_DIR_MODE) < 0)
diff --git a/src/refs.c b/src/refs.c
index 9dc422e..9249391 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -262,14 +262,15 @@ static int loose_write(git_reference *ref)
 	if (git_buf_joinpath(&ref_path, ref->owner->path_repository, ref->name) < 0)
 		return -1;
 
-	/* Remove a possibly existing empty directory hierarchy 
+	/* Remove a possibly existing empty directory hierarchy
 	 * which name would collide with the reference name
 	 */
-	if (git_path_isdir(git_buf_cstr(&ref_path)) && 
-		(git_futils_rmdir_r(git_buf_cstr(&ref_path), GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0)) {
-			git_buf_free(&ref_path);
-			return -1;
-		}
+	if (git_path_isdir(git_buf_cstr(&ref_path)) &&
+		git_futils_rmdir_r(git_buf_cstr(&ref_path), NULL,
+			GIT_DIRREMOVAL_ONLY_EMPTY_DIRS) < 0) {
+		git_buf_free(&ref_path);
+		return -1;
+	}
 
 	if (git_filebuf_open(&file, ref_path.ptr, GIT_FILEBUF_FORCE) < 0) {
 		git_buf_free(&ref_path);
diff --git a/src/status.c b/src/status.c
index e39006e..50ac19d 100644
--- a/src/status.c
+++ b/src/status.c
@@ -100,7 +100,7 @@ int git_status_foreach_ext(
 	memset(&diffopt, 0, sizeof(diffopt));
 	memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
 
-	diffopt.flags = GIT_DIFF_DONT_SPLIT_TYPECHANGE;
+	diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
 
 	if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0)
 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
diff --git a/src/tree.c b/src/tree.c
index 83aa303..8d3f266 100644
--- a/src/tree.c
+++ b/src/tree.c
@@ -180,9 +180,9 @@ void git_tree__free(git_tree *tree)
 	git__free(tree);
 }
 
-const git_oid *git_tree_id(git_tree *c)
+const git_oid *git_tree_id(const git_tree *c)
 {
-	return git_object_id((git_object *)c);
+	return git_object_id((const git_object *)c);
 }
 
 git_filemode_t git_tree_entry_filemode(const git_tree_entry *entry)
@@ -286,7 +286,7 @@ int git_tree__prefix_position(git_tree *tree, const char *path)
 	return at_pos;
 }
 
-unsigned int git_tree_entrycount(git_tree *tree)
+unsigned int git_tree_entrycount(const git_tree *tree)
 {
 	assert(tree);
 	return (unsigned int)tree->entries.length;
diff --git a/tests-clar/checkout/typechange.c b/tests-clar/checkout/typechange.c
new file mode 100644
index 0000000..f013617
--- /dev/null
+++ b/tests-clar/checkout/typechange.c
@@ -0,0 +1,72 @@
+#include "clar_libgit2.h"
+#include "git2/checkout.h"
+#include "path.h"
+#include "posix.h"
+
+static git_repository *g_repo = NULL;
+
+static const char *g_typechange_oids[] = {
+	"79b9f23e85f55ea36a472a902e875bc1121a94cb",
+	"9bdb75b73836a99e3dbeea640a81de81031fdc29",
+	"0e7ed140b514b8cae23254cb8656fe1674403aff",
+	"9d0235c7a7edc0889a18f97a42ee6db9fe688447",
+	"9b19edf33a03a0c59cdfc113bfa5c06179bf9b1a",
+	"1b63caae4a5ca96f78e8dfefc376c6a39a142475",
+	"6eae26c90e8ccc4d16208972119c40635489c6f0",
+	NULL
+};
+
+static bool g_typechange_empty[] = {
+	true, false, false, false, false, false, true, true
+};
+
+void test_checkout_typechange__initialize(void)
+{
+	g_repo = cl_git_sandbox_init("typechanges");
+
+	cl_fixture_sandbox("submod2_target");
+	p_rename("submod2_target/.gitted", "submod2_target/.git");
+}
+
+void test_checkout_typechange__cleanup(void)
+{
+	cl_git_sandbox_cleanup();
+	cl_fixture_cleanup("submod2_target");
+}
+
+void test_checkout_typechange__checkout_typechanges(void)
+{
+	int i;
+	git_object *obj;
+	git_checkout_opts opts = {0};
+
+	opts.checkout_strategy =
+		GIT_CHECKOUT_REMOVE_UNTRACKED |
+		GIT_CHECKOUT_CREATE_MISSING |
+		GIT_CHECKOUT_OVERWRITE_MODIFIED;
+
+	for (i = 0; g_typechange_oids[i] != NULL; ++i) {
+		cl_git_pass(git_revparse_single(&obj, g_repo, g_typechange_oids[i]));
+		/* fprintf(stderr, "checking out '%s'\n", g_typechange_oids[i]); */
+
+		cl_git_pass(git_checkout_tree(g_repo, obj, &opts, NULL));
+
+		git_object_free(obj);
+
+		if (!g_typechange_empty[i]) {
+			cl_assert(git_path_isdir("typechanges"));
+			cl_assert(git_path_exists("typechanges/a"));
+			cl_assert(git_path_exists("typechanges/b"));
+			cl_assert(git_path_exists("typechanges/c"));
+			cl_assert(git_path_exists("typechanges/d"));
+			cl_assert(git_path_exists("typechanges/e"));
+		} else {
+			cl_assert(git_path_isdir("typechanges"));
+			cl_assert(!git_path_exists("typechanges/a"));
+			cl_assert(!git_path_exists("typechanges/b"));
+			cl_assert(!git_path_exists("typechanges/c"));
+			cl_assert(!git_path_exists("typechanges/d"));
+			cl_assert(!git_path_exists("typechanges/e"));
+		}
+	}
+}
diff --git a/tests-clar/core/copy.c b/tests-clar/core/copy.c
index 2fdfed8..d0b21f6 100644
--- a/tests-clar/core/copy.c
+++ b/tests-clar/core/copy.c
@@ -41,7 +41,7 @@ void test_core_copy__file_in_dir(void)
 	cl_assert(S_ISREG(st.st_mode));
 	cl_assert(strlen(content) == (size_t)st.st_size);
 
-	cl_git_pass(git_futils_rmdir_r("an_dir", GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r("an_dir", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	cl_assert(!git_path_isdir("an_dir"));
 }
 
@@ -95,7 +95,7 @@ void test_core_copy__tree(void)
 	cl_assert(S_ISLNK(st.st_mode));
 #endif
 
-	cl_git_pass(git_futils_rmdir_r("t1", GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r("t1", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	cl_assert(!git_path_isdir("t1"));
 
 	/* copy with empty dirs, no links, yes dotfiles, no overwrite */
@@ -119,8 +119,8 @@ void test_core_copy__tree(void)
 	cl_git_fail(git_path_lstat("t2/c/d/l1", &st));
 #endif
 
-	cl_git_pass(git_futils_rmdir_r("t2", GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r("t2", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	cl_assert(!git_path_isdir("t2"));
 
-	cl_git_pass(git_futils_rmdir_r("src", GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r("src", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 }
diff --git a/tests-clar/core/mkdir.c b/tests-clar/core/mkdir.c
index 08ba241..e5dc665 100644
--- a/tests-clar/core/mkdir.c
+++ b/tests-clar/core/mkdir.c
@@ -6,11 +6,11 @@
 static void cleanup_basic_dirs(void *ref)
 {
 	GIT_UNUSED(ref);
-	git_futils_rmdir_r("d0", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
-	git_futils_rmdir_r("d1", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
-	git_futils_rmdir_r("d2", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
-	git_futils_rmdir_r("d3", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
-	git_futils_rmdir_r("d4", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("d0", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("d1", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("d2", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("d3", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("d4", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
 }
 
 void test_core_mkdir__basic(void)
@@ -56,7 +56,7 @@ void test_core_mkdir__basic(void)
 static void cleanup_basedir(void *ref)
 {
 	GIT_UNUSED(ref);
-	git_futils_rmdir_r("base", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("base", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
 }
 
 void test_core_mkdir__with_base(void)
@@ -108,7 +108,7 @@ static void cleanup_chmod_root(void *ref)
 		git__free(mode);
 	}
 
-	git_futils_rmdir_r("r", GIT_DIRREMOVAL_EMPTY_HIERARCHY);
+	git_futils_rmdir_r("r", NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY);
 }
 
 static void check_mode(mode_t expected, mode_t actual)
diff --git a/tests-clar/core/rmdir.c b/tests-clar/core/rmdir.c
index 530f1f9..9ada8f4 100644
--- a/tests-clar/core/rmdir.c
+++ b/tests-clar/core/rmdir.c
@@ -30,7 +30,7 @@ void test_core_rmdir__initialize(void)
 /* make sure empty dir can be deleted recusively */
 void test_core_rmdir__delete_recursive(void)
 {
-	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
+	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
 }
 
 /* make sure non-empty dir cannot be deleted recusively */
@@ -42,10 +42,10 @@ void test_core_rmdir__fail_to_delete_non_empty_dir(void)
 
 	cl_git_mkfile(git_buf_cstr(&file), "dummy");
 
-	cl_git_fail(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
+	cl_git_fail(git_futils_rmdir_r(empty_tmp_dir, NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
 
 	cl_must_pass(p_unlink(file.ptr));
-	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
+	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, NULL, GIT_DIRREMOVAL_EMPTY_HIERARCHY));
 
 	git_buf_free(&file);
 }
@@ -58,10 +58,10 @@ void test_core_rmdir__can_skip__non_empty_dir(void)
 
 	cl_git_mkfile(git_buf_cstr(&file), "dummy");
 
-	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS));
+	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, NULL, GIT_DIRREMOVAL_ONLY_EMPTY_DIRS));
 	cl_assert(git_path_exists(git_buf_cstr(&file)) == true);
 
-	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(empty_tmp_dir, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	cl_assert(git_path_exists(empty_tmp_dir) == false);
 
 	git_buf_free(&file);
diff --git a/tests-clar/diff/iterator.c b/tests-clar/diff/iterator.c
index c27d3fa..023bc46 100644
--- a/tests-clar/diff/iterator.c
+++ b/tests-clar/diff/iterator.c
@@ -1,6 +1,7 @@
 #include "clar_libgit2.h"
 #include "diff_helpers.h"
 #include "iterator.h"
+#include "tree.h"
 
 void test_diff_iterator__initialize(void)
 {
@@ -237,6 +238,103 @@ void test_diff_iterator__tree_range_empty_2(void)
 		NULL, ".aaa_empty_before", 0, NULL);
 }
 
+static void check_tree_entry(
+	git_iterator *i,
+	const char *oid,
+	const char *oid_p,
+	const char *oid_pp,
+	const char *oid_ppp)
+{
+	const git_index_entry *ie;
+	const git_tree_entry *te;
+	const git_tree *tree;
+	git_buf path = GIT_BUF_INIT;
+
+	cl_git_pass(git_iterator_current_tree_entry(i, &te));
+	cl_assert(te);
+	cl_assert(git_oid_streq(&te->oid, oid) == 0);
+
+	cl_git_pass(git_iterator_current(i, &ie));
+	cl_git_pass(git_buf_sets(&path, ie->path));
+
+	if (oid_p) {
+		git_buf_rtruncate_at_char(&path, '/');
+		cl_git_pass(git_iterator_current_parent_tree(i, path.ptr, &tree));
+		cl_assert(tree);
+		cl_assert(git_oid_streq(git_tree_id(tree), oid_p) == 0);
+	}
+
+	if (oid_pp) {
+		git_buf_rtruncate_at_char(&path, '/');
+		cl_git_pass(git_iterator_current_parent_tree(i, path.ptr, &tree));
+		cl_assert(tree);
+		cl_assert(git_oid_streq(git_tree_id(tree), oid_pp) == 0);
+	}
+
+	if (oid_ppp) {
+		git_buf_rtruncate_at_char(&path, '/');
+		cl_git_pass(git_iterator_current_parent_tree(i, path.ptr, &tree));
+		cl_assert(tree);
+		cl_assert(git_oid_streq(git_tree_id(tree), oid_ppp) == 0);
+	}
+
+	git_buf_free(&path);
+}
+
+void test_diff_iterator__tree_special_functions(void)
+{
+	git_tree *t;
+	git_iterator *i;
+	const git_index_entry *entry;
+	git_repository *repo = cl_git_sandbox_init("attr");
+	int cases = 0;
+	const char *rootoid = "ce39a97a7fb1fa90bcf5e711249c1e507476ae0e";
+
+	t = resolve_commit_oid_to_tree(
+		repo, "24fa9a9fc4e202313e24b648087495441dab432b");
+	cl_assert(t != NULL);
+
+	cl_git_pass(git_iterator_for_tree_range(&i, repo, t, NULL, NULL));
+	cl_git_pass(git_iterator_current(i, &entry));
+
+	while (entry != NULL) {
+		if (strcmp(entry->path, "sub/file") == 0) {
+			cases++;
+			check_tree_entry(
+				i, "45b983be36b73c0788dc9cbcb76cbb80fc7bb057",
+				"ecb97df2a174987475ac816e3847fc8e9f6c596b",
+				rootoid, NULL);
+		}
+		else if (strcmp(entry->path, "sub/sub/subsub.txt") == 0) {
+			cases++;
+			check_tree_entry(
+				i, "9e5bdc47d6a80f2be0ea3049ad74231b94609242",
+				"4e49ba8c5b6c32ff28cd9dcb60be34df50fcc485",
+				"ecb97df2a174987475ac816e3847fc8e9f6c596b", rootoid);
+		}
+		else if (strcmp(entry->path, "subdir/.gitattributes") == 0) {
+			cases++;
+			check_tree_entry(
+				i, "99eae476896f4907224978b88e5ecaa6c5bb67a9",
+				"9fb40b6675dde60b5697afceae91b66d908c02d9",
+				rootoid, NULL);
+		}
+		else if (strcmp(entry->path, "subdir2/subdir2_test1") == 0) {
+			cases++;
+			check_tree_entry(
+				i, "dccada462d3df8ac6de596fb8c896aba9344f941",
+				"2929de282ce999e95183aedac6451d3384559c4b",
+				rootoid, NULL);
+		}
+
+		cl_git_pass(git_iterator_advance(i, &entry));
+	}
+
+	cl_assert_equal_i(4, cases);
+	git_iterator_free(i);
+	git_tree_free(t);
+}
+
 /* -- INDEX ITERATOR TESTS -- */
 
 static void index_iterator_test(
diff --git a/tests-clar/object/blob/write.c b/tests-clar/object/blob/write.c
index 722c7b9..87a9e20 100644
--- a/tests-clar/object/blob/write.c
+++ b/tests-clar/object/blob/write.c
@@ -49,7 +49,7 @@ void test_object_blob_write__can_create_a_blob_in_a_standard_repo_from_a_absolut
 	assert_blob_creation(ELSEWHERE "/test.txt", git_buf_cstr(&full_path), &git_blob_create_fromdisk);
 
 	git_buf_free(&full_path);
-	cl_must_pass(git_futils_rmdir_r(ELSEWHERE, GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_must_pass(git_futils_rmdir_r(ELSEWHERE, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 }
 
 void test_object_blob_write__can_create_a_blob_in_a_bare_repo_from_a_absolute_filepath(void)
@@ -65,5 +65,5 @@ void test_object_blob_write__can_create_a_blob_in_a_bare_repo_from_a_absolute_fi
 	assert_blob_creation(ELSEWHERE "/test.txt", git_buf_cstr(&full_path), &git_blob_create_fromdisk);
 
 	git_buf_free(&full_path);
-	cl_must_pass(git_futils_rmdir_r(ELSEWHERE, GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_must_pass(git_futils_rmdir_r(ELSEWHERE, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 }
diff --git a/tests-clar/repo/discover.c b/tests-clar/repo/discover.c
index b3d639b..b5afab7 100644
--- a/tests-clar/repo/discover.c
+++ b/tests-clar/repo/discover.c
@@ -135,7 +135,7 @@ void test_repo_discover__0(void)
 	ensure_repository_discover(REPOSITORY_ALTERNATE_FOLDER_SUB_SUB, ceiling_dirs, sub_repository_path);
 	ensure_repository_discover(REPOSITORY_ALTERNATE_FOLDER_SUB_SUB_SUB, ceiling_dirs, repository_path);
 
-	cl_git_pass(git_futils_rmdir_r(TEMP_REPO_FOLDER, GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(TEMP_REPO_FOLDER, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	git_repository_free(repo);
 	git_buf_free(&ceiling_dirs_buf);
 }
diff --git a/tests-clar/repo/open.c b/tests-clar/repo/open.c
index c70ec83..ef912fa 100644
--- a/tests-clar/repo/open.c
+++ b/tests-clar/repo/open.c
@@ -7,7 +7,7 @@ void test_repo_open__cleanup(void)
 	cl_git_sandbox_cleanup();
 
 	if (git_path_isdir("alternate"))
-		git_futils_rmdir_r("alternate", GIT_DIRREMOVAL_FILES_AND_DIRS);
+		git_futils_rmdir_r("alternate", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS);
 }
 
 void test_repo_open__bare_empty_repo(void)
@@ -202,8 +202,8 @@ void test_repo_open__bad_gitlinks(void)
 		cl_git_fail(git_repository_open_ext(&repo, "alternate", 0, NULL));
 	}
 
-	git_futils_rmdir_r("invalid", GIT_DIRREMOVAL_FILES_AND_DIRS);
-	git_futils_rmdir_r("invalid2", GIT_DIRREMOVAL_FILES_AND_DIRS);
+	git_futils_rmdir_r("invalid", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS);
+	git_futils_rmdir_r("invalid2", NULL, GIT_DIRREMOVAL_FILES_AND_DIRS);
 }
 
 #ifdef GIT_WIN32
diff --git a/tests-clar/resources/typechanges/.gitted/index b/tests-clar/resources/typechanges/.gitted/index
index 2f4c6d7..4f6d12a 100644
Binary files a/tests-clar/resources/typechanges/.gitted/index and b/tests-clar/resources/typechanges/.gitted/index differ
diff --git a/tests-clar/resources/typechanges/README.md b/tests-clar/resources/typechanges/README.md
index 99e8bab..1f5a95a 100644
--- a/tests-clar/resources/typechanges/README.md
+++ b/tests-clar/resources/typechanges/README.md
@@ -1,32 +1,43 @@
 This is a test repo for libgit2 where tree entries have type changes
 
+Types
+-----
+
 The key types that could be found in tree entries are:
 
-1 - GIT_FILEMODE_NEW             = 0000000
-2 - GIT_FILEMODE_TREE            = 0040000
-3 - GIT_FILEMODE_BLOB            = 0100644
-4 - GIT_FILEMODE_BLOB_EXECUTABLE = 0100755
-5 - GIT_FILEMODE_LINK            = 0120000
-6 - GIT_FILEMODE_COMMIT          = 0160000
+1. GIT_FILEMODE_NEW             = 0000000 (i.e. file does not exist)
+2. GIT_FILEMODE_TREE            = 0040000
+3. GIT_FILEMODE_BLOB            = 0100644
+4. GIT_FILEMODE_BLOB_EXECUTABLE = 0100755
+5. GIT_FILEMODE_LINK            = 0120000
+6. GIT_FILEMODE_COMMIT          = 0160000
 
 I will try to have every type of transition somewhere in the history
 of this repo.
 
 Commits
 -------
-Initial commit - a(1)    b(1)    c(1)    d(1)    e(1)
-  79b9f23e85f55ea36a472a902e875bc1121a94cb
-Create content - a(1->2) b(1->3) c(1->4) d(1->5) e(1->6)
-  9bdb75b73836a99e3dbeea640a81de81031fdc29
-Changes #1     - a(2->3) b(3->4) c(4->5) d(5->6) e(6->2)
-  0e7ed140b514b8cae23254cb8656fe1674403aff
-Changes #2     - a(3->5) b(4->6) c(5->2) d(6->3) e(2->4)
-  9d0235c7a7edc0889a18f97a42ee6db9fe688447
-Changes #3     - a(5->3) b(6->4) c(2->5) d(3->6) e(4->2)
-  9b19edf33a03a0c59cdfc113bfa5c06179bf9b1a
-Changes #4     - a(3->2) b(4->3) c(5->4) d(6->5) e(2->6)
-  1b63caae4a5ca96f78e8dfefc376c6a39a142475
-  Matches "Changes #1" except README.md
-Changes #5     - a(2->1) b(3->1) c(4->1) d(5->1) e(6->1)
-  6eae26c90e8ccc4d16208972119c40635489c6f0
-  Matches "Initial commit" except README.md and .gitmodules
+
+* `a(1--1) b(1--1) c(1--1) d(1--1) e(1--1)`
+  **Initial commit**<br>
+  `79b9f23e85f55ea36a472a902e875bc1121a94cb`
+* `a(1->2) b(1->3) c(1->4) d(1->5) e(1->6)`
+  **Create content**<br>
+  `9bdb75b73836a99e3dbeea640a81de81031fdc29`
+* `a(2->3) b(3->4) c(4->5) d(5->6) e(6->2)`
+  **Changes #1**<br>
+  `0e7ed140b514b8cae23254cb8656fe1674403aff`
+* `a(3->5) b(4->6) c(5->2) d(6->3) e(2->4)`
+  **Changes #2**<br>
+  `9d0235c7a7edc0889a18f97a42ee6db9fe688447`
+* `a(5->3) b(6->4) c(2->5) d(3->6) e(4->2)`
+  **Changes #3**<br>
+  `9b19edf33a03a0c59cdfc113bfa5c06179bf9b1a`
+* `a(3->2) b(4->3) c(5->4) d(6->5) e(2->6)`
+  **Changes #4**<br>
+  `1b63caae4a5ca96f78e8dfefc376c6a39a142475`<br>
+  Matches **Changes #1** except README.md
+* `a(2->1) b(3->1) c(4->1) d(5->1) e(6->1)`
+  **Changes #5**<br>
+  `6eae26c90e8ccc4d16208972119c40635489c6f0`<br>
+  Matches **Initial commit** except README.md and .gitmodules
diff --git a/tests-clar/status/worktree.c b/tests-clar/status/worktree.c
index 0ceba7f..4f03643 100644
--- a/tests-clar/status/worktree.c
+++ b/tests-clar/status/worktree.c
@@ -71,7 +71,7 @@ static int remove_file_cb(void *data, git_buf *file)
 		return 0;
 
 	if (git_path_isdir(filename))
-		cl_git_pass(git_futils_rmdir_r(filename, GIT_DIRREMOVAL_FILES_AND_DIRS));
+		cl_git_pass(git_futils_rmdir_r(filename, NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	else
 		cl_git_pass(p_unlink(git_buf_cstr(file)));
 
@@ -314,7 +314,7 @@ void test_status_worktree__issue_592_3(void)
 	repo = cl_git_sandbox_init("issue_592");
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(repo), "c"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 
 	cl_git_pass(git_status_foreach(repo, cb_status__check_592, "c/a.txt"));
 
@@ -344,7 +344,7 @@ void test_status_worktree__issue_592_5(void)
 	repo = cl_git_sandbox_init("issue_592");
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(repo), "t"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 	cl_git_pass(p_mkdir(git_buf_cstr(&path), 0777));
 
 	cl_git_pass(git_status_foreach(repo, cb_status__check_592, NULL));
diff --git a/tests-clar/submodule/status.c b/tests-clar/submodule/status.c
index d3a3923..63073ce 100644
--- a/tests-clar/submodule/status.c
+++ b/tests-clar/submodule/status.c
@@ -50,7 +50,7 @@ void test_submodule_status__ignore_none(void)
 	git_buf path = GIT_BUF_INIT;
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(g_repo), "sm_unchanged"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 
 	cl_git_fail(git_submodule_lookup(&sm, g_repo, "not_submodule"));
 
@@ -135,7 +135,7 @@ void test_submodule_status__ignore_untracked(void)
 	git_submodule_ignore_t ign = GIT_SUBMODULE_IGNORE_UNTRACKED;
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(g_repo), "sm_unchanged"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 
 	cl_git_pass(git_submodule_foreach(g_repo, set_sm_ignore, &ign));
 
@@ -195,7 +195,7 @@ void test_submodule_status__ignore_dirty(void)
 	git_submodule_ignore_t ign = GIT_SUBMODULE_IGNORE_DIRTY;
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(g_repo), "sm_unchanged"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 
 	cl_git_pass(git_submodule_foreach(g_repo, set_sm_ignore, &ign));
 
@@ -255,7 +255,7 @@ void test_submodule_status__ignore_all(void)
 	git_submodule_ignore_t ign = GIT_SUBMODULE_IGNORE_ALL;
 
 	cl_git_pass(git_buf_joinpath(&path, git_repository_workdir(g_repo), "sm_unchanged"));
-	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), GIT_DIRREMOVAL_FILES_AND_DIRS));
+	cl_git_pass(git_futils_rmdir_r(git_buf_cstr(&path), NULL, GIT_DIRREMOVAL_FILES_AND_DIRS));
 
 	cl_git_pass(git_submodule_foreach(g_repo, set_sm_ignore, &ign));