Skip to content

Commit 1b56cda

Browse files
author
Edward Thomson
committed
Merge pull request libgit2#3770 from libgit2/cmn/tree-update
Add a method specifically for modifying trees
2 parents c148533 + 9464f9e commit 1b56cda

File tree

3 files changed

+458
-0
lines changed

3 files changed

+458
-0
lines changed

include/git2/tree.h

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,52 @@ GIT_EXTERN(int) git_tree_walk(
418418
*/
419419
GIT_EXTERN(int) git_tree_dup(git_tree **out, git_tree *source);
420420

421+
/**
422+
* The kind of update to perform
423+
*/
424+
typedef enum {
425+
/** Update or insert an entry at the specified path */
426+
GIT_TREE_UPDATE_UPSERT,
427+
/** Remove an entry from the specified path */
428+
GIT_TREE_UPDATE_REMOVE,
429+
} git_tree_update_t;
430+
431+
/**
432+
* An action to perform during the update of a tree
433+
*/
434+
typedef struct {
435+
/** Update action. If it's an removal, only the path is looked at */
436+
git_tree_update_t action;
437+
/** The entry's id */
438+
git_oid id;
439+
/** The filemode/kind of object */
440+
git_filemode_t filemode;
441+
/** The full path from the root tree */
442+
const char *path;
443+
} git_tree_update;
444+
445+
/**
446+
* Create a tree based on another one with the specified modifications
447+
*
448+
* Given the `baseline` perform the changes described in the list of
449+
* `updates` and create a new tree.
450+
*
451+
* This function is optimized for common file/directory addition, removal and
452+
* replacement in trees. It is much more efficient than reading the tree into a
453+
* `git_index` and modifying that, but in exchange it is not as flexible.
454+
*
455+
* Deleting and adding the same entry is undefined behaviour, changing
456+
* a tree to a blob or viceversa is not supported.
457+
*
458+
* @param out id of the new tree
459+
* @param repo the repository in which to create the tree, must be the
460+
* same as for `baseline`
461+
* @param baseline the tree to base these changes on
462+
* @param nupdates the number of elements in the update list
463+
* @param updates the list of updates to perform
464+
*/
465+
GIT_EXTERN(int) git_tree_create_updated(git_oid *out, git_repository *repo, git_tree *baseline, size_t nupdates, const git_tree_update *updates);
466+
421467
/** @} */
422468

423469
GIT_END_DECL

src/tree.c

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,3 +1034,248 @@ int git_tree_walk(
10341034
return error;
10351035
}
10361036

1037+
static int compare_entries(const void *_a, const void *_b)
1038+
{
1039+
const git_tree_update *a = (git_tree_update *) _a;
1040+
const git_tree_update *b = (git_tree_update *) _b;
1041+
1042+
return strcmp(a->path, b->path);
1043+
}
1044+
1045+
static int on_dup_entry(void **old, void *new)
1046+
{
1047+
GIT_UNUSED(old); GIT_UNUSED(new);
1048+
1049+
giterr_set(GITERR_TREE, "duplicate entries given for update");
1050+
return -1;
1051+
}
1052+
1053+
/*
1054+
* We keep the previous tree and the new one at each level of the
1055+
* stack. When we leave a level we're done with that tree and we can
1056+
* write it out to the odb.
1057+
*/
1058+
typedef struct {
1059+
git_treebuilder *bld;
1060+
git_tree *tree;
1061+
char *name;
1062+
} tree_stack_entry;
1063+
1064+
/** Count how many slashes (i.e. path components) there are in this string */
1065+
GIT_INLINE(size_t) count_slashes(const char *path)
1066+
{
1067+
size_t count = 0;
1068+
const char *slash;
1069+
1070+
while ((slash = strchr(path, '/')) != NULL) {
1071+
count++;
1072+
path = slash + 1;
1073+
}
1074+
1075+
return count;
1076+
}
1077+
1078+
static bool next_component(git_buf *out, const char *in)
1079+
{
1080+
const char *slash = strchr(in, '/');
1081+
1082+
git_buf_clear(out);
1083+
1084+
if (slash)
1085+
git_buf_put(out, in, slash - in);
1086+
1087+
return !!slash;
1088+
}
1089+
1090+
static int create_popped_tree(tree_stack_entry *current, tree_stack_entry *popped, git_buf *component)
1091+
{
1092+
int error;
1093+
git_oid new_tree;
1094+
1095+
git_tree_free(popped->tree);
1096+
error = git_treebuilder_write(&new_tree, popped->bld);
1097+
git_treebuilder_free(popped->bld);
1098+
1099+
if (error < 0) {
1100+
git__free(popped->name);
1101+
return error;
1102+
}
1103+
1104+
/* We've written out the tree, now we have to put the new value into its parent */
1105+
git_buf_clear(component);
1106+
git_buf_puts(component, popped->name);
1107+
git__free(popped->name);
1108+
1109+
GITERR_CHECK_ALLOC(component->ptr);
1110+
1111+
/* Error out if this would create a D/F conflict in this update */
1112+
if (current->tree) {
1113+
const git_tree_entry *to_replace;
1114+
to_replace = git_tree_entry_byname(current->tree, component->ptr);
1115+
if (to_replace && git_tree_entry_type(to_replace) != GIT_OBJ_TREE) {
1116+
giterr_set(GITERR_TREE, "D/F conflict when updating tree");
1117+
return -1;
1118+
}
1119+
}
1120+
1121+
return git_treebuilder_insert(NULL, current->bld, component->ptr, &new_tree, GIT_FILEMODE_TREE);
1122+
}
1123+
1124+
int git_tree_create_updated(git_oid *out, git_repository *repo, git_tree *baseline, size_t nupdates, const git_tree_update *updates)
1125+
{
1126+
git_array_t(tree_stack_entry) stack = GIT_ARRAY_INIT;
1127+
tree_stack_entry *root_elem;
1128+
git_vector entries;
1129+
int error;
1130+
size_t i;
1131+
git_buf component = GIT_BUF_INIT;
1132+
1133+
if ((error = git_vector_init(&entries, nupdates, compare_entries)) < 0)
1134+
return error;
1135+
1136+
/* Sort the entries for treversal */
1137+
for (i = 0 ; i < nupdates; i++) {
1138+
if ((error = git_vector_insert_sorted(&entries, (void *) &updates[i], on_dup_entry)) < 0)
1139+
goto cleanup;
1140+
}
1141+
1142+
root_elem = git_array_alloc(stack);
1143+
GITERR_CHECK_ALLOC(root_elem);
1144+
memset(root_elem, 0, sizeof(*root_elem));
1145+
1146+
if (baseline && (error = git_tree_dup(&root_elem->tree, baseline)) < 0)
1147+
goto cleanup;
1148+
1149+
if ((error = git_treebuilder_new(&root_elem->bld, repo, root_elem->tree)) < 0)
1150+
goto cleanup;
1151+
1152+
for (i = 0; i < nupdates; i++) {
1153+
const git_tree_update *last_update = i == 0 ? NULL : &updates[i-1];
1154+
const git_tree_update *update = &updates[i];
1155+
size_t common_prefix = 0, steps_up, j;
1156+
const char *path;
1157+
1158+
/* Figure out how much we need to change from the previous tree */
1159+
if (last_update)
1160+
common_prefix = git_path_common_dirlen(last_update->path, update->path);
1161+
1162+
/*
1163+
* The entries are sorted, so when we find we're no
1164+
* longer in the same directory, we need to abandon
1165+
* the old tree (steps up) and dive down to the next
1166+
* one.
1167+
*/
1168+
steps_up = last_update == NULL ? 0 : count_slashes(&last_update->path[common_prefix]);
1169+
1170+
for (j = 0; j < steps_up; j++) {
1171+
tree_stack_entry *current, *popped = git_array_pop(stack);
1172+
assert(popped);
1173+
1174+
current = git_array_last(stack);
1175+
assert(current);
1176+
1177+
if ((error = create_popped_tree(current, popped, &component)) < 0)
1178+
goto cleanup;
1179+
}
1180+
1181+
/* Now that we've created the trees we popped from the stack, let's go back down */
1182+
path = &update->path[common_prefix];
1183+
while (next_component(&component, path)) {
1184+
tree_stack_entry *last, *new_entry;
1185+
const git_tree_entry *entry;
1186+
1187+
last = git_array_last(stack);
1188+
entry = last->tree ? git_tree_entry_byname(last->tree, component.ptr) : NULL;
1189+
if (entry && git_tree_entry_type(entry) != GIT_OBJ_TREE) {
1190+
giterr_set(GITERR_TREE, "D/F conflict when updating tree");
1191+
error = -1;
1192+
goto cleanup;
1193+
}
1194+
1195+
new_entry = git_array_alloc(stack);
1196+
GITERR_CHECK_ALLOC(new_entry);
1197+
memset(new_entry, 0, sizeof(*new_entry));
1198+
1199+
new_entry->tree = NULL;
1200+
if (entry && (error = git_tree_lookup(&new_entry->tree, repo, git_tree_entry_id(entry))) < 0)
1201+
goto cleanup;
1202+
1203+
if ((error = git_treebuilder_new(&new_entry->bld, repo, new_entry->tree)) < 0)
1204+
goto cleanup;
1205+
1206+
new_entry->name = git__strdup(component.ptr);
1207+
GITERR_CHECK_ALLOC(new_entry->name);
1208+
1209+
/* Get to the start of the next component */
1210+
path += component.size + 1;
1211+
}
1212+
1213+
/* After all that, we're finally at the place where we want to perform the update */
1214+
switch (update->action) {
1215+
case GIT_TREE_UPDATE_UPSERT:
1216+
{
1217+
/* Make sure we're replacing something of the same type */
1218+
tree_stack_entry *last = git_array_last(stack);
1219+
const char *basename = git_path_basename(update->path);
1220+
const git_tree_entry *e = git_treebuilder_get(last->bld, basename);
1221+
if (e && git_tree_entry_type(e) != git_object__type_from_filemode(update->filemode)) {
1222+
giterr_set(GITERR_TREE, "Cannot replace '%s' with '%s' at '%s'",
1223+
git_object_type2string(git_tree_entry_type(e)),
1224+
git_object_type2string(git_object__type_from_filemode(update->filemode)),
1225+
update->path);
1226+
return -1;
1227+
}
1228+
1229+
error = git_treebuilder_insert(NULL, last->bld, basename, &update->id, update->filemode);
1230+
break;
1231+
}
1232+
case GIT_TREE_UPDATE_REMOVE:
1233+
error = git_treebuilder_remove(git_array_last(stack)->bld, update->path);
1234+
break;
1235+
default:
1236+
giterr_set(GITERR_TREE, "unkown action for update");
1237+
error = -1;
1238+
goto cleanup;
1239+
}
1240+
1241+
if (error < 0)
1242+
goto cleanup;
1243+
}
1244+
1245+
/* We're done, go up the stack again and write out the tree */
1246+
{
1247+
tree_stack_entry *current = NULL, *popped = NULL;
1248+
while ((popped = git_array_pop(stack)) != NULL) {
1249+
current = git_array_last(stack);
1250+
/* We've reached the top, current is the root tree */
1251+
if (!current)
1252+
break;
1253+
1254+
if ((error = create_popped_tree(current, popped, &component)) < 0)
1255+
goto cleanup;
1256+
}
1257+
1258+
/* Write out the root tree */
1259+
git__free(popped->name);
1260+
git_tree_free(popped->tree);
1261+
1262+
error = git_treebuilder_write(out, popped->bld);
1263+
git_treebuilder_free(popped->bld);
1264+
if (error < 0)
1265+
goto cleanup;
1266+
}
1267+
1268+
cleanup:
1269+
{
1270+
tree_stack_entry *e;
1271+
while ((e = git_array_pop(stack)) != NULL) {
1272+
git_treebuilder_free(e->bld);
1273+
git_tree_free(e->tree);
1274+
git__free(e->name);
1275+
}
1276+
}
1277+
1278+
git_array_clear(stack);
1279+
git_vector_free(&entries);
1280+
return error;
1281+
}

0 commit comments

Comments
 (0)