From 83862c83748b03d473a58d983916bec0bc385b62 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Mon, 17 Feb 2020 21:28:13 +0000 Subject: commit-graph: Add a way to write commit-graph files This change adds the git_commit_graph_writer_* functions to allow to write and create `commit-graph` files from `.idx`/`.pack` files or `git_revwalk`s. Part of: #5757 --- include/git2/sys/commit_graph.h | 132 +++++++- include/git2/types.h | 3 + src/commit_graph.c | 646 +++++++++++++++++++++++++++++++++++++++- src/commit_graph.h | 15 + tests/graph/commit_graph.c | 35 +++ 5 files changed, 829 insertions(+), 2 deletions(-) diff --git a/include/git2/sys/commit_graph.h b/include/git2/sys/commit_graph.h index 038c9b739..01cf8d1bf 100644 --- a/include/git2/sys/commit_graph.h +++ b/include/git2/sys/commit_graph.h @@ -40,6 +40,136 @@ GIT_EXTERN(int) git_commit_graph_open(git_commit_graph **cgraph_out, const char */ GIT_EXTERN(void) git_commit_graph_free(git_commit_graph *cgraph); -GIT_END_DECL +/** + * Create a new writer for `commit-graph` files. + * + * @param out Location to store the writer pointer. + * @param objects_info_dir The `objects/info` directory. + * The `commit-graph` file will be written in this directory. + * @return 0 or an error code + */ +GIT_EXTERN(int) git_commit_graph_writer_new( + git_commit_graph_writer **out, + const char *objects_info_dir); + +/** + * Free the commit-graph writer and its resources. + * + * @param w The writer to free. If NULL no action is taken. + */ +GIT_EXTERN(void) git_commit_graph_writer_free(git_commit_graph_writer *w); + +/** + * Add an `.idx` file (associated to a packfile) to the writer. + * + * @param w The writer. + * @param repo The repository that owns the `.idx` file. + * @param idx_path The path of an `.idx` file. + * @return 0 or an error code + */ +GIT_EXTERN(int) git_commit_graph_writer_add_index_file( + git_commit_graph_writer *w, + git_repository *repo, + const char *idx_path); + +/** + * Add a revwalk to the writer. This will add all the commits from the revwalk + * to the commit-graph. + * + * @param w The writer. + * @param walk The git_revwalk. + * @return 0 or an error code + */ +GIT_EXTERN(int) git_commit_graph_writer_add_revwalk( + git_commit_graph_writer *w, + git_revwalk *walk); + +/** + * The strategy to use when adding a new set of commits to a pre-existing + * commit-graph chain. + */ +typedef enum { + /** + * Do not split commit-graph files. The other split strategy-related option + * fields are ignored. + */ + GIT_COMMIT_GRAPH_SPLIT_STRATEGY_SINGLE_FILE = 0, +} git_commit_graph_split_strategy_t; + +/** + * Options structure for + * `git_commit_graph_writer_commit`/`git_commit_graph_writer_dump`. + * + * Initialize with `GIT_COMMIT_GRAPH_WRITER_OPTIONS_INIT`. Alternatively, you + * can use `git_commit_graph_writer_options_init`. + */ +typedef struct { + unsigned int version; + + /** + * The strategy to use when adding new commits to a pre-existing commit-graph + * chain. + */ + git_commit_graph_split_strategy_t split_strategy; + + /** + * The number of commits in level N is less than X times the number of + * commits in level N + 1. + */ + float size_multiple; + + /** + * The number of commits in level N + 1 is more than C commits. + */ + size_t max_commits; +} git_commit_graph_writer_options; + +#define GIT_COMMIT_GRAPH_WRITER_OPTIONS_VERSION 1 +#define GIT_COMMIT_GRAPH_WRITER_OPTIONS_INIT \ + { \ + GIT_COMMIT_GRAPH_WRITER_OPTIONS_VERSION, \ + GIT_COMMIT_GRAPH_SPLIT_STRATEGY_SINGLE_FILE, 2.0f, 64000 \ + } + +/** + * Initialize git_commit_graph_writer_options structure + * + * Initializes a `git_commit_graph_writer_options` with default values. Equivalent to + * creating an instance with `GIT_COMMIT_GRAPH_WRITER_OPTIONS_INIT`. + * + * @param opts The `git_commit_graph_writer_options` struct to initialize. + * @param version The struct version; pass `GIT_COMMIT_GRAPH_WRITER_OPTIONS_VERSION`. + * @return Zero on success; -1 on failure. + */ +GIT_EXTERN(int) git_commit_graph_writer_options_init( + git_commit_graph_writer_options *opts, + unsigned int version); + +/** + * Write a `commit-graph` file to a file. + * + * @param w The writer + * @param opts Pointer to git_commit_graph_writer_options struct. + * @return 0 or an error code + */ +GIT_EXTERN(int) git_commit_graph_writer_commit( + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts); + +/** + * Dump the contents of the `commit-graph` to an in-memory buffer. + * + * @param buffer Buffer where to store the contents of the `commit-graph`. + * @param w The writer. + * @param opts Pointer to git_commit_graph_writer_options struct. + * @return 0 or an error code + */ +GIT_EXTERN(int) git_commit_graph_writer_dump( + git_buf *buffer, + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts); + +/** @} */ +GIT_END_DECL #endif diff --git a/include/git2/types.h b/include/git2/types.h index 562eb8e5f..4d5c72324 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -96,6 +96,9 @@ typedef struct git_odb_stream git_odb_stream; /** A stream to write a packfile to the ODB */ typedef struct git_odb_writepack git_odb_writepack; +/** a writer for commit-graph files. */ +typedef struct git_commit_graph_writer git_commit_graph_writer; + /** An open refs database handle. */ typedef struct git_refdb git_refdb; diff --git a/src/commit_graph.c b/src/commit_graph.c index 556925600..3d1c03639 100644 --- a/src/commit_graph.c +++ b/src/commit_graph.c @@ -7,11 +7,19 @@ #include "commit_graph.h" +#include "array.h" +#include "filebuf.h" #include "futils.h" #include "hash.h" +#include "oidarray.h" +#include "oidmap.h" #include "pack.h" +#include "repository.h" +#include "revwalk.h" #define GIT_COMMIT_GRAPH_MISSING_PARENT 0x70000000 +#define GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX 0x3FFFFFFF +#define GIT_COMMIT_GRAPH_GENERATION_NUMBER_INFINITY 0xFFFFFFFF #define COMMIT_GRAPH_SIGNATURE 0x43475048 /* "CGPH" */ #define COMMIT_GRAPH_VERSION 1 @@ -36,6 +44,63 @@ struct git_commit_graph_chunk { size_t length; }; +typedef git_array_t(size_t) parent_index_array_t; + +struct packed_commit { + size_t index; + git_oid sha1; + git_oid tree_oid; + uint32_t generation; + git_time_t commit_time; + git_array_oid_t parents; + parent_index_array_t parent_indices; +}; + +static void packed_commit_free(struct packed_commit *p) +{ + if (!p) + return; + + git_array_clear(p->parents); + git_array_clear(p->parent_indices); + git__free(p); +} + +static struct packed_commit *packed_commit_new(git_commit *commit) +{ + unsigned int i, parentcount = git_commit_parentcount(commit); + struct packed_commit *p + = (struct packed_commit *)git__calloc(1, sizeof(struct packed_commit)); + if (!p) + goto cleanup; + + git_array_init_to_size(p->parents, parentcount); + if (parentcount && !p->parents.ptr) + goto cleanup; + + if (git_oid_cpy(&p->sha1, git_commit_id(commit)) < 0) + goto cleanup; + if (git_oid_cpy(&p->tree_oid, git_commit_tree_id(commit)) < 0) + goto cleanup; + p->commit_time = git_commit_time(commit); + + for (i = 0; i < parentcount; ++i) { + git_oid *parent_id = git_array_alloc(p->parents); + if (!parent_id) + goto cleanup; + if (git_oid_cpy(parent_id, git_commit_parent_id(commit, i)) < 0) + goto cleanup; + } + + return p; + +cleanup: + packed_commit_free(p); + return NULL; +} + +typedef int (*commit_graph_write_cb)(const char *buf, size_t size, void *cb_data); + static int commit_graph_error(const char *message) { git_error_set(GIT_ERROR_ODB, "invalid commit-graph file - %s", message); @@ -401,7 +466,6 @@ static int git_commit_graph_entry_get_byindex( extra_edge_list_pos++; e->parent_count++; } - } git_oid_cpy(&e->sha1, &file->oid_lookup[pos]); return 0; @@ -547,3 +611,583 @@ void git_commit_graph_file_free(git_commit_graph_file *file) git_commit_graph_file_close(file); git__free(file); } + +static int packed_commit__cmp(const void *a_, const void *b_) +{ + const struct packed_commit *a = a_; + const struct packed_commit *b = b_; + return git_oid_cmp(&a->sha1, &b->sha1); +} + +int git_commit_graph_writer_new(git_commit_graph_writer **out, const char *objects_info_dir) +{ + git_commit_graph_writer *w = git__calloc(1, sizeof(git_commit_graph_writer)); + GIT_ERROR_CHECK_ALLOC(w); + + if (git_buf_sets(&w->objects_info_dir, objects_info_dir) < 0) { + git__free(w); + return -1; + } + + if (git_vector_init(&w->commits, 0, packed_commit__cmp) < 0) { + git_buf_dispose(&w->objects_info_dir); + git__free(w); + return -1; + } + + *out = w; + return 0; +} + +void git_commit_graph_writer_free(git_commit_graph_writer *w) +{ + struct packed_commit *packed_commit; + size_t i; + + if (!w) + return; + + git_vector_foreach (&w->commits, i, packed_commit) + packed_commit_free(packed_commit); + git_vector_free(&w->commits); + git_buf_dispose(&w->objects_info_dir); + git__free(w); +} + +struct object_entry_cb_state { + git_repository *repo; + git_odb *db; + git_vector *commits; +}; + +static int object_entry__cb(const git_oid *id, void *data) +{ + struct object_entry_cb_state *state = (struct object_entry_cb_state *)data; + git_commit *commit = NULL; + struct packed_commit *packed_commit = NULL; + size_t header_len; + git_object_t header_type; + int error = 0; + + error = git_odb_read_header(&header_len, &header_type, state->db, id); + if (error < 0) + return error; + + if (header_type != GIT_OBJECT_COMMIT) + return 0; + + error = git_commit_lookup(&commit, state->repo, id); + if (error < 0) + return error; + + packed_commit = packed_commit_new(commit); + git_commit_free(commit); + GIT_ERROR_CHECK_ALLOC(packed_commit); + + error = git_vector_insert(state->commits, packed_commit); + if (error < 0) { + packed_commit_free(packed_commit); + return error; + } + + return 0; +} + +int git_commit_graph_writer_add_index_file( + git_commit_graph_writer *w, + git_repository *repo, + const char *idx_path) +{ + int error; + struct git_pack_file *p = NULL; + struct object_entry_cb_state state = { + .repo = repo, + .db = NULL, + .commits = &w->commits, + }; + + error = git_repository_odb(&state.db, repo); + if (error < 0) + goto cleanup; + + error = git_mwindow_get_pack(&p, idx_path); + if (error < 0) + goto cleanup; + + error = git_pack_foreach_entry(p, object_entry__cb, &state); + if (error < 0) + goto cleanup; + +cleanup: + git_mwindow_put_pack(p); + git_odb_free(state.db); + return error; +} + +int git_commit_graph_writer_add_revwalk(git_commit_graph_writer *w, git_revwalk *walk) +{ + int error; + git_oid id; + git_repository *repo = git_revwalk_repository(walk); + git_commit *commit; + struct packed_commit* packed_commit; + + while ((git_revwalk_next(&id, walk)) == 0) { + error = git_commit_lookup(&commit, repo, &id); + if (error < 0) + return error; + + packed_commit = packed_commit_new(commit); + git_commit_free(commit); + GIT_ERROR_CHECK_ALLOC(packed_commit); + + error = git_vector_insert(&w->commits, packed_commit); + if (error < 0) { + packed_commit_free(packed_commit); + return error; + } + } + + return 0; +} + +enum generation_number_commit_state { + GENERATION_NUMBER_COMMIT_STATE_UNVISITED = 0, + GENERATION_NUMBER_COMMIT_STATE_ADDED = 1, + GENERATION_NUMBER_COMMIT_STATE_EXPANDED = 2, + GENERATION_NUMBER_COMMIT_STATE_VISITED = 3, +}; + +static int compute_generation_numbers(git_vector *commits) +{ + git_array_t(size_t) index_stack = GIT_ARRAY_INIT; + size_t i, j; + size_t *parent_idx; + enum generation_number_commit_state *commit_states = NULL; + struct packed_commit *child_packed_commit; + git_oidmap *packed_commit_map = NULL; + int error = 0; + + /* First populate the parent indices fields */ + error = git_oidmap_new(&packed_commit_map); + if (error < 0) + goto cleanup; + git_vector_foreach (commits, i, child_packed_commit) { + child_packed_commit->index = i; + error = git_oidmap_set( + packed_commit_map, &child_packed_commit->sha1, child_packed_commit); + if (error < 0) + goto cleanup; + } + + git_vector_foreach (commits, i, child_packed_commit) { + size_t parent_i, *parent_idx_ptr; + struct packed_commit *parent_packed_commit; + git_oid *parent_id; + git_array_init_to_size( + child_packed_commit->parent_indices, + git_array_size(child_packed_commit->parents)); + if (git_array_size(child_packed_commit->parents) + && !child_packed_commit->parent_indices.ptr) { + error = -1; + goto cleanup; + } + git_array_foreach (child_packed_commit->parents, parent_i, parent_id) { + parent_packed_commit = git_oidmap_get(packed_commit_map, parent_id); + if (!parent_packed_commit) { + git_error_set(GIT_ERROR_ODB, + "parent commit %s not found in commit graph", + git_oid_tostr_s(parent_id)); + error = GIT_ENOTFOUND; + goto cleanup; + } + parent_idx_ptr = git_array_alloc(child_packed_commit->parent_indices); + if (!parent_idx_ptr) { + error = -1; + goto cleanup; + } + *parent_idx_ptr = parent_packed_commit->index; + } + } + + /* + * We copy all the commits to the stack and then during visitation, + * each node can be added up to two times to the stack. + */ + git_array_init_to_size(index_stack, 3 * git_vector_length(commits)); + if (!index_stack.ptr) { + error = -1; + goto cleanup; + } + + commit_states = (enum generation_number_commit_state *)git__calloc( + git_vector_length(commits), sizeof(enum generation_number_commit_state)); + if (!commit_states) { + error = -1; + goto cleanup; + } + + /* + * Perform a Post-Order traversal so that all parent nodes are fully + * visited before the child node. + */ + git_vector_foreach (commits, i, child_packed_commit) + *(size_t *)git_array_alloc(index_stack) = i; + + while (git_array_size(index_stack)) { + i = *git_array_pop(index_stack); + child_packed_commit = git_vector_get(commits, i); + + if (commit_states[i] == GENERATION_NUMBER_COMMIT_STATE_VISITED) { + /* This commit has already been fully visited. */ + continue; + } + if (commit_states[i] == GENERATION_NUMBER_COMMIT_STATE_EXPANDED) { + /* All of the commits parents have been visited. */ + child_packed_commit->generation = 0; + git_array_foreach (child_packed_commit->parent_indices, j, parent_idx) { + struct packed_commit *parent = git_vector_get(commits, *parent_idx); + if (child_packed_commit->generation < parent->generation) + child_packed_commit->generation = parent->generation; + } + if (child_packed_commit->generation + < GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX) { + ++child_packed_commit->generation; + } + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_VISITED; + continue; + } + + /* + * This is the first time we see this commit. We need + * to visit all its parents before we can fully visit + * it. + */ + if (git_array_size(child_packed_commit->parent_indices) == 0) { + /* + * Special case: if the commit has no parents, there's + * no need to add it to the stack just to immediately + * remove it. + */ + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_VISITED; + child_packed_commit->generation = 1; + continue; + } + + /* + * Add this current commit again so that it is visited + * again once all its children have been visited. + */ + *(size_t *)git_array_alloc(index_stack) = i; + git_array_foreach (child_packed_commit->parent_indices, j, parent_idx) { + if (commit_states[*parent_idx] + != GENERATION_NUMBER_COMMIT_STATE_UNVISITED) { + /* This commit has already been considered. */ + continue; + } + + commit_states[*parent_idx] = GENERATION_NUMBER_COMMIT_STATE_ADDED; + *(size_t *)git_array_alloc(index_stack) = *parent_idx; + } + commit_states[i] = GENERATION_NUMBER_COMMIT_STATE_EXPANDED; + } + +cleanup: + git_oidmap_free(packed_commit_map); + git__free(commit_states); + git_array_clear(index_stack); + + return error; +} + +static int write_offset(off64_t offset, commit_graph_write_cb write_cb, void *cb_data) +{ + int error; + uint32_t word; + + word = htonl((uint32_t)((offset >> 32) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + word = htonl((uint32_t)((offset >> 0) & 0xffffffffu)); + error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + + return 0; +} + +static int +write_chunk_header(int chunk_id, off64_t offset, commit_graph_write_cb write_cb, void *cb_data) +{ + uint32_t word = htonl(chunk_id); + int error = write_cb((const char *)&word, sizeof(word), cb_data); + if (error < 0) + return error; + return write_offset(offset, write_cb, cb_data); +} + +static int commit_graph_write_buf(const char *buf, size_t size, void *data) +{ + git_buf *b = (git_buf *)data; + return git_buf_put(b, buf, size); +} + +struct commit_graph_write_hash_context { + commit_graph_write_cb write_cb; + void *cb_data; + git_hash_ctx *ctx; +}; + +static int commit_graph_write_hash(const char *buf, size_t size, void *data) +{ + struct commit_graph_write_hash_context *ctx + = (struct commit_graph_write_hash_context *)data; + int error; + + error = git_hash_update(ctx->ctx, buf, size); + if (error < 0) + return error; + + return ctx->write_cb(buf, size, ctx->cb_data); +} + +static void packed_commit_free_dup(void *packed_commit) +{ + packed_commit_free(packed_commit); +} + +static int +commit_graph_write(git_commit_graph_writer *w, commit_graph_write_cb write_cb, void *cb_data) +{ + int error = 0; + size_t i; + struct packed_commit *packed_commit; + struct git_commit_graph_header hdr = { + .signature = htonl(COMMIT_GRAPH_SIGNATURE), + .version = COMMIT_GRAPH_VERSION, + .object_id_version = COMMIT_GRAPH_OBJECT_ID_VERSION, + .chunks = 0, + .base_graph_files = 0, + }; + uint32_t oid_fanout_count; + uint32_t extra_edge_list_count; + uint32_t oid_fanout[256]; + off64_t offset; + git_buf oid_lookup = GIT_BUF_INIT, + commit_data = GIT_BUF_INIT, extra_edge_list = GIT_BUF_INIT; + git_oid cgraph_checksum = {{0}}; + git_hash_ctx ctx; + struct commit_graph_write_hash_context hash_cb_data = { + .write_cb = write_cb, + .cb_data = cb_data, + .ctx = &ctx, + }; + + error = git_hash_ctx_init(&ctx); + if (error < 0) + return error; + cb_data = &hash_cb_data; + write_cb = commit_graph_write_hash; + + /* Sort the commits. */ + git_vector_sort(&w->commits); + git_vector_uniq(&w->commits, packed_commit_free_dup); + error = compute_generation_numbers(&w->commits); + if (error < 0) + goto cleanup; + + /* Fill the OID Fanout table. */ + oid_fanout_count = 0; + for (i = 0; i < 256; i++) { + while (oid_fanout_count < git_vector_length(&w->commits) + && ((struct packed_commit *)git_vector_get(&w->commits, oid_fanout_count)) + ->sha1.id[0] + <= i) + ++oid_fanout_count; + oid_fanout[i] = htonl(oid_fanout_count); + } + + /* Fill the OID Lookup table. */ + git_vector_foreach (&w->commits, i, packed_commit) { + error = git_buf_put( + &oid_lookup, (const char *)&packed_commit->sha1, sizeof(git_oid)); + if (error < 0) + goto cleanup; + } + + /* Fill the Commit Data and Extra Edge List tables. */ + extra_edge_list_count = 0; + git_vector_foreach (&w->commits, i, packed_commit) { + uint64_t commit_time; + uint32_t generation; + uint32_t word; + unsigned int parentcount = (unsigned int)git_array_size(packed_commit->parents); + + error + = git_buf_put(&commit_data, + (const char *)&packed_commit->tree_oid, + sizeof(git_oid)); + if (error < 0) + goto cleanup; + + if (parentcount == 0) + word = htonl(GIT_COMMIT_GRAPH_MISSING_PARENT); + else + word = htonl((uint32_t)*git_array_get(packed_commit->parent_indices, 0)); + error = git_buf_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + + if (parentcount < 2) + word = htonl(GIT_COMMIT_GRAPH_MISSING_PARENT); + else if (parentcount == 2) + word = htonl((uint32_t)*git_array_get(packed_commit->parent_indices, 1)); + else + word = htonl(0x80000000u | extra_edge_list_count); + error = git_buf_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + + if (parentcount > 2) { + unsigned int parent_i; + for (parent_i = 1; parent_i < parentcount; ++parent_i) { + word = htonl((uint32_t)( + *git_array_get(packed_commit->parent_indices, parent_i) + | (parent_i + 1 == parentcount ? 0x80000000u : 0))); + error + = git_buf_put(&extra_edge_list, + (const char *)&word, + sizeof(word)); + if (error < 0) + goto cleanup; + } + extra_edge_list_count += parentcount - 1; + } + + generation = packed_commit->generation; + commit_time = (uint64_t)packed_commit->commit_time; + if (generation > GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX) + generation = GIT_COMMIT_GRAPH_GENERATION_NUMBER_MAX; + word = ntohl((uint32_t)((generation << 2) | ((commit_time >> 32ull) & 0x3ull))); + error = git_buf_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + word = ntohl((uint32_t)(commit_time & 0xffffffffull)); + error = git_buf_put(&commit_data, (const char *)&word, sizeof(word)); + if (error < 0) + goto cleanup; + } + + /* Write the header. */ + hdr.chunks = 3; + if (git_buf_len(&extra_edge_list) > 0) + hdr.chunks++; + error = write_cb((const char *)&hdr, sizeof(hdr), cb_data); + if (error < 0) + goto cleanup; + + /* Write the chunk headers. */ + offset = sizeof(hdr) + (hdr.chunks + 1) * 12; + error = write_chunk_header(COMMIT_GRAPH_OID_FANOUT_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += sizeof(oid_fanout); + error = write_chunk_header(COMMIT_GRAPH_OID_LOOKUP_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_buf_len(&oid_lookup); + error = write_chunk_header(COMMIT_GRAPH_COMMIT_DATA_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_buf_len(&commit_data); + if (git_buf_len(&extra_edge_list) > 0) { + error = write_chunk_header( + COMMIT_GRAPH_EXTRA_EDGE_LIST_ID, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + offset += git_buf_len(&extra_edge_list); + } + error = write_chunk_header(0, offset, write_cb, cb_data); + if (error < 0) + goto cleanup; + + /* Write all the chunks. */ + error = write_cb((const char *)oid_fanout, sizeof(oid_fanout), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_buf_cstr(&oid_lookup), git_buf_len(&oid_lookup), cb_data); + if (error < 0) + goto cleanup; + error = write_cb(git_buf_cstr(&commit_data), git_buf_len(&commit_data), cb_data); + if (error < 0) + goto cleanup; + error + = write_cb(git_buf_cstr(&extra_edge_list), + git_buf_len(&extra_edge_list), + cb_data); + if (error < 0) + goto cleanup; + + /* Finalize the checksum and write the trailer. */ + error = git_hash_final(&cgraph_checksum, &ctx); + if (error < 0) + goto cleanup; + error = write_cb((const char *)&cgraph_checksum, sizeof(cgraph_checksum), cb_data); + if (error < 0) + goto cleanup; + +cleanup: + git_buf_dispose(&oid_lookup); + git_buf_dispose(&commit_data); + git_buf_dispose(&extra_edge_list); + git_hash_ctx_cleanup(&ctx); + return error; +} + +static int commit_graph_write_filebuf(const char *buf, size_t size, void *data) +{ + git_filebuf *f = (git_filebuf *)data; + return git_filebuf_write(f, buf, size); +} + +int git_commit_graph_writer_commit( + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts) +{ + int error; + int filebuf_flags = GIT_FILEBUF_DO_NOT_BUFFER; + git_buf commit_graph_path = GIT_BUF_INIT; + git_filebuf output = GIT_FILEBUF_INIT; + + GIT_UNUSED(opts); + + error = git_buf_joinpath( + &commit_graph_path, git_buf_cstr(&w->objects_info_dir), "commit-graph"); + if (error < 0) + return error; + + if (git_repository__fsync_gitdir) + filebuf_flags |= GIT_FILEBUF_FSYNC; + error = git_filebuf_open(&output, git_buf_cstr(&commit_graph_path), filebuf_flags, 0644); + git_buf_dispose(&commit_graph_path); + if (error < 0) + return error; + + error = commit_graph_write(w, commit_graph_write_filebuf, &output); + if (error < 0) { + git_filebuf_cleanup(&output); + return error; + } + + return git_filebuf_commit(&output); +} + +int git_commit_graph_writer_dump( + git_buf *cgraph, + git_commit_graph_writer *w, + git_commit_graph_writer_options *opts) +{ + GIT_UNUSED(opts); + return commit_graph_write(w, commit_graph_write_buf, cgraph); +} diff --git a/src/commit_graph.h b/src/commit_graph.h index e5e3ea1fd..9d0a827de 100644 --- a/src/commit_graph.h +++ b/src/commit_graph.h @@ -14,6 +14,7 @@ #include "git2/sys/commit_graph.h" #include "map.h" +#include "vector.h" /** * A commit-graph file. @@ -118,6 +119,20 @@ int git_commit_graph_get_file(git_commit_graph_file **file_out, git_commit_graph /* Marks the commit-graph file as needing a refresh. */ void git_commit_graph_refresh(git_commit_graph *cgraph); +/* + * A writer for `commit-graph` files. + */ +struct git_commit_graph_writer { + /* + * The path of the `objects/info` directory where the `commit-graph` will be + * stored. + */ + git_buf objects_info_dir; + + /* The list of packed commits. */ + git_vector commits; +}; + /* * Returns whether the git_commit_graph_file needs to be reloaded since the * contents of the commit-graph file have changed on disk. diff --git a/tests/graph/commit_graph.c b/tests/graph/commit_graph.c index 2ff157eb4..5926dca6f 100644 --- a/tests/graph/commit_graph.c +++ b/tests/graph/commit_graph.c @@ -1,8 +1,10 @@ #include "clar_libgit2.h" #include +#include #include "commit_graph.h" +#include "futils.h" void test_graph_commit_graph__parse(void) { @@ -88,3 +90,36 @@ void test_graph_commit_graph__parse_octopus_merge(void) git_repository_free(repo); git_buf_dispose(&commit_graph_path); } + +void test_graph_commit_graph__writer(void) +{ + git_repository *repo; + git_commit_graph_writer *w = NULL; + git_revwalk *walk; + git_commit_graph_writer_options opts = GIT_COMMIT_GRAPH_WRITER_OPTIONS_INIT; + git_buf cgraph = GIT_BUF_INIT, expected_cgraph = GIT_BUF_INIT, path = GIT_BUF_INIT; + + cl_git_pass(git_repository_open(&repo, cl_fixture("testrepo.git"))); + + cl_git_pass(git_buf_joinpath(&path, git_repository_path(repo), "objects/info")); + cl_git_pass(git_commit_graph_writer_new(&w, git_buf_cstr(&path))); + + /* This is equivalent to `git commit-graph write --reachable`. */ + cl_git_pass(git_revwalk_new(&walk, repo)); + cl_git_pass(git_revwalk_push_glob(walk, "refs/*")); + cl_git_pass(git_commit_graph_writer_add_revwalk(w, walk)); + git_revwalk_free(walk); + + cl_git_pass(git_commit_graph_writer_dump(&cgraph, w, &opts)); + cl_git_pass(git_buf_joinpath(&path, git_repository_path(repo), "objects/info/commit-graph")); + cl_git_pass(git_futils_readbuffer(&expected_cgraph, git_buf_cstr(&path))); + + cl_assert_equal_i(git_buf_len(&cgraph), git_buf_len(&expected_cgraph)); + cl_assert_equal_i(memcmp(git_buf_cstr(&cgraph), git_buf_cstr(&expected_cgraph), git_buf_len(&cgraph)), 0); + + git_buf_dispose(&cgraph); + git_buf_dispose(&expected_cgraph); + git_buf_dispose(&path); + git_commit_graph_writer_free(w); + git_repository_free(repo); +} -- cgit v1.2.1