Merge branch 'ds/commit-graph-incremental'
authorJunio C Hamano <gitster@pobox.com>
Fri, 19 Jul 2019 18:30:20 +0000 (11:30 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 19 Jul 2019 18:30:20 +0000 (11:30 -0700)
The commits in a repository can be described by multiple
commit-graph files now, which allows the commit-graph files to be
updated incrementally.

* ds/commit-graph-incremental:
commit-graph: test verify across alternates
commit-graph: normalize commit-graph filenames
commit-graph: test --split across alternate without --split
commit-graph: test octopus merges with --split
commit-graph: clean up chains after flattened write
commit-graph: verify chains with --shallow mode
commit-graph: create options for split files
commit-graph: expire commit-graph files
commit-graph: allow cross-alternate chains
commit-graph: merge commit-graph chains
commit-graph: add --split option to builtin
commit-graph: write commit-graph chains
commit-graph: rearrange chunk count logic
commit-graph: add base graphs chunk
commit-graph: load commit-graph chains
commit-graph: rename commit_compare to oid_compare
commit-graph: prepare for commit-graph chains
commit-graph: document commit-graph chains

Documentation/git-commit-graph.txt
Documentation/technical/commit-graph-format.txt
Documentation/technical/commit-graph.txt
builtin/commit-graph.c
builtin/commit.c
builtin/gc.c
commit-graph.c
commit-graph.h
t/t5318-commit-graph.sh
t/t5324-split-commit-graph.sh [new file with mode: 0755]
index 624470e198202a47e91e89274bb32b2bc3d441d6..eb5e7865f0ef787e1410a95702990e38567b68fe 100644 (file)
@@ -10,7 +10,7 @@ SYNOPSIS
 --------
 [verse]
 'git commit-graph read' [--object-dir <dir>]
-'git commit-graph verify' [--object-dir <dir>]
+'git commit-graph verify' [--object-dir <dir>] [--shallow]
 'git commit-graph write' <options> [--object-dir <dir>]
 
 
@@ -26,7 +26,7 @@ OPTIONS
        Use given directory for the location of packfiles and commit-graph
        file. This parameter exists to specify the location of an alternate
        that only has the objects directory, not a full `.git` directory. The
-       commit-graph file is expected to be at `<dir>/info/commit-graph` and
+       commit-graph file is expected to be in the `<dir>/info` directory and
        the packfiles are expected to be in `<dir>/pack`.
 
 
@@ -51,6 +51,25 @@ or `--stdin-packs`.)
 +
 With the `--append` option, include all commits that are present in the
 existing commit-graph file.
++
+With the `--split` option, write the commit-graph as a chain of multiple
+commit-graph files stored in `<dir>/info/commit-graphs`. The new commits
+not already in the commit-graph are added in a new "tip" file. This file
+is merged with the existing file if the following merge conditions are
+met:
++
+* If `--size-multiple=<X>` is not specified, let `X` equal 2. If the new
+tip file would have `N` commits and the previous tip has `M` commits and
+`X` times `N` is greater than  `M`, instead merge the two files into a
+single file.
++
+* If `--max-commits=<M>` is specified with `M` a positive integer, and the
+new tip file would have more than `M` commits, then instead merge the new
+tip with the previous tip.
++
+Finally, if `--expire-time=<datetime>` is not specified, let `datetime`
+be the current time. After writing the split commit-graph, delete all
+unused commit-graph whose modified times are older than `datetime`.
 
 'read'::
 
@@ -61,6 +80,9 @@ Used for debugging purposes.
 
 Read the commit-graph file and verify its contents against the object
 database. Used to check for corrupted data.
++
+With the `--shallow` option, only check the tip commit-graph file in
+a chain of split commit-graphs.
 
 
 EXAMPLES
index 16452a0504c8fa5b9b1b62cb907b96315db43942..a4f17441aed30f14c036a4bed6a911c86cf31ce5 100644 (file)
@@ -44,8 +44,9 @@ HEADER:
 
   1-byte number (C) of "chunks"
 
-  1-byte (reserved for later use)
-     Current clients should ignore this value.
+  1-byte number (B) of base commit-graphs
+      We infer the length (H*B) of the Base Graphs chunk
+      from this value.
 
 CHUNK LOOKUP:
 
@@ -92,6 +93,12 @@ CHUNK DATA:
       positions for the parents until reaching a value with the most-significant
       bit on. The other bits correspond to the position of the last parent.
 
+  Base Graphs List (ID: {'B', 'A', 'S', 'E'}) [Optional]
+      This list of H-byte hashes describe a set of B commit-graph files that
+      form a commit-graph chain. The graph position for the ith commit in this
+      file's OID Lookup chunk is equal to i plus the number of commits in all
+      base graphs.  If B is non-zero, this chunk must exist.
+
 TRAILER:
 
        H-byte HASH-checksum of all of the above.
index fb53341d5ee3116361d599b5aca2e5ea3a228589..729fbcb32f8793d06da3a985bb6d8a299b3a15dd 100644 (file)
@@ -127,6 +127,197 @@ Design Details
   helpful for these clones, anyway. The commit-graph will not be read or
   written when shallow commits are present.
 
+Commit Graphs Chains
+--------------------
+
+Typically, repos grow with near-constant velocity (commits per day). Over time,
+the number of commits added by a fetch operation is much smaller than the
+number of commits in the full history. By creating a "chain" of commit-graphs,
+we enable fast writes of new commit data without rewriting the entire commit
+history -- at least, most of the time.
+
+## File Layout
+
+A commit-graph chain uses multiple files, and we use a fixed naming convention
+to organize these files. Each commit-graph file has a name
+`$OBJDIR/info/commit-graphs/graph-{hash}.graph` where `{hash}` is the hex-
+valued hash stored in the footer of that file (which is a hash of the file's
+contents before that hash). For a chain of commit-graph files, a plain-text
+file at `$OBJDIR/info/commit-graphs/commit-graph-chain` contains the
+hashes for the files in order from "lowest" to "highest".
+
+For example, if the `commit-graph-chain` file contains the lines
+
+```
+       {hash0}
+       {hash1}
+       {hash2}
+```
+
+then the commit-graph chain looks like the following diagram:
+
+ +-----------------------+
+ |  graph-{hash2}.graph  |
+ +-----------------------+
+         |
+ +-----------------------+
+ |                       |
+ |  graph-{hash1}.graph  |
+ |                       |
+ +-----------------------+
+         |
+ +-----------------------+
+ |                       |
+ |                       |
+ |                       |
+ |  graph-{hash0}.graph  |
+ |                       |
+ |                       |
+ |                       |
+ +-----------------------+
+
+Let X0 be the number of commits in `graph-{hash0}.graph`, X1 be the number of
+commits in `graph-{hash1}.graph`, and X2 be the number of commits in
+`graph-{hash2}.graph`. If a commit appears in position i in `graph-{hash2}.graph`,
+then we interpret this as being the commit in position (X0 + X1 + i), and that
+will be used as its "graph position". The commits in `graph-{hash2}.graph` use these
+positions to refer to their parents, which may be in `graph-{hash1}.graph` or
+`graph-{hash0}.graph`. We can navigate to an arbitrary commit in position j by checking
+its containment in the intervals [0, X0), [X0, X0 + X1), [X0 + X1, X0 + X1 +
+X2).
+
+Each commit-graph file (except the base, `graph-{hash0}.graph`) contains data
+specifying the hashes of all files in the lower layers. In the above example,
+`graph-{hash1}.graph` contains `{hash0}` while `graph-{hash2}.graph` contains
+`{hash0}` and `{hash1}`.
+
+## Merging commit-graph files
+
+If we only added a new commit-graph file on every write, we would run into a
+linear search problem through many commit-graph files.  Instead, we use a merge
+strategy to decide when the stack should collapse some number of levels.
+
+The diagram below shows such a collapse. As a set of new commits are added, it
+is determined by the merge strategy that the files should collapse to
+`graph-{hash1}`. Thus, the new commits, the commits in `graph-{hash2}` and
+the commits in `graph-{hash1}` should be combined into a new `graph-{hash3}`
+file.
+
+                           +---------------------+
+                           |                     |
+                           |    (new commits)    |
+                           |                     |
+                           +---------------------+
+                           |                     |
+ +-----------------------+  +---------------------+
+ |  graph-{hash2} |->|                     |
+ +-----------------------+  +---------------------+
+         |                 |                     |
+ +-----------------------+  +---------------------+
+ |                       |  |                     |
+ |  graph-{hash1} |->|                     |
+ |                       |  |                     |
+ +-----------------------+  +---------------------+
+         |                  tmp_graphXXX
+ +-----------------------+
+ |                       |
+ |                       |
+ |                       |
+ |  graph-{hash0} |
+ |                       |
+ |                       |
+ |                       |
+ +-----------------------+
+
+During this process, the commits to write are combined, sorted and we write the
+contents to a temporary file, all while holding a `commit-graph-chain.lock`
+lock-file.  When the file is flushed, we rename it to `graph-{hash3}`
+according to the computed `{hash3}`. Finally, we write the new chain data to
+`commit-graph-chain.lock`:
+
+```
+       {hash3}
+       {hash0}
+```
+
+We then close the lock-file.
+
+## Merge Strategy
+
+When writing a set of commits that do not exist in the commit-graph stack of
+height N, we default to creating a new file at level N + 1. We then decide to
+merge with the Nth level if one of two conditions hold:
+
+  1. `--size-multiple=<X>` is specified or X = 2, and the number of commits in
+     level N is less than X times the number of commits in level N + 1.
+
+  2. `--max-commits=<C>` is specified with non-zero C and the number of commits
+     in level N + 1 is more than C commits.
+
+This decision cascades down the levels: when we merge a level we create a new
+set of commits that then compares to the next level.
+
+The first condition bounds the number of levels to be logarithmic in the total
+number of commits.  The second condition bounds the total number of commits in
+a `graph-{hashN}` file and not in the `commit-graph` file, preventing
+significant performance issues when the stack merges and another process only
+partially reads the previous stack.
+
+The merge strategy values (2 for the size multiple, 64,000 for the maximum
+number of commits) could be extracted into config settings for full
+flexibility.
+
+## Deleting graph-{hash} files
+
+After a new tip file is written, some `graph-{hash}` files may no longer
+be part of a chain. It is important to remove these files from disk, eventually.
+The main reason to delay removal is that another process could read the
+`commit-graph-chain` file before it is rewritten, but then look for the
+`graph-{hash}` files after they are deleted.
+
+To allow holding old split commit-graphs for a while after they are unreferenced,
+we update the modified times of the files when they become unreferenced. Then,
+we scan the `$OBJDIR/info/commit-graphs/` directory for `graph-{hash}`
+files whose modified times are older than a given expiry window. This window
+defaults to zero, but can be changed using command-line arguments or a config
+setting.
+
+## Chains across multiple object directories
+
+In a repo with alternates, we look for the `commit-graph-chain` file starting
+in the local object directory and then in each alternate. The first file that
+exists defines our chain. As we look for the `graph-{hash}` files for
+each `{hash}` in the chain file, we follow the same pattern for the host
+directories.
+
+This allows commit-graphs to be split across multiple forks in a fork network.
+The typical case is a large "base" repo with many smaller forks.
+
+As the base repo advances, it will likely update and merge its commit-graph
+chain more frequently than the forks. If a fork updates their commit-graph after
+the base repo, then it should "reparent" the commit-graph chain onto the new
+chain in the base repo. When reading each `graph-{hash}` file, we track
+the object directory containing it. During a write of a new commit-graph file,
+we check for any changes in the source object directory and read the
+`commit-graph-chain` file for that source and create a new file based on those
+files. During this "reparent" operation, we necessarily need to collapse all
+levels in the fork, as all of the files are invalid against the new base file.
+
+It is crucial to be careful when cleaning up "unreferenced" `graph-{hash}.graph`
+files in this scenario. It falls to the user to define the proper settings for
+their custom environment:
+
+ 1. When merging levels in the base repo, the unreferenced files may still be
+    referenced by chains from fork repos.
+
+ 2. The expiry time should be set to a length of time such that every fork has
+    time to recompute their commit-graph chain to "reparent" onto the new base
+    file(s).
+
+ 3. If the commit-graph chain is updated in the base, the fork will not have
+    access to the new chain until its chain is updated to reference those files.
+    (This may change in the future [5].)
+
 Related Links
 -------------
 [0] https://bugs.chromium.org/p/git/issues/detail?id=8
@@ -153,3 +344,7 @@ Related Links
 
 [4] https://public-inbox.org/git/20180108154822.54829-1-git@jeffhostetler.com/T/#u
     A patch to remove the ahead-behind calculation from 'status'.
+
+[5] https://public-inbox.org/git/f27db281-abad-5043-6d71-cbb083b1c877@gmail.com/
+    A discussion of a "two-dimensional graph position" that can allow reading
+    multiple commit-graph chains at the same time.
index d8efa5bab276a816bef48e9cb3891ccc0cb81aad..38027b83d9d8329a1dc2e47b236a985e4ce71060 100644 (file)
@@ -5,17 +5,18 @@
 #include "parse-options.h"
 #include "repository.h"
 #include "commit-graph.h"
+#include "object-store.h"
 
 static char const * const builtin_commit_graph_usage[] = {
        N_("git commit-graph [--object-dir <objdir>]"),
        N_("git commit-graph read [--object-dir <objdir>]"),
-       N_("git commit-graph verify [--object-dir <objdir>]"),
-       N_("git commit-graph write [--object-dir <objdir>] [--append] [--reachable|--stdin-packs|--stdin-commits]"),
+       N_("git commit-graph verify [--object-dir <objdir>] [--shallow]"),
+       N_("git commit-graph write [--object-dir <objdir>] [--append|--split] [--reachable|--stdin-packs|--stdin-commits] <split options>"),
        NULL
 };
 
 static const char * const builtin_commit_graph_verify_usage[] = {
-       N_("git commit-graph verify [--object-dir <objdir>]"),
+       N_("git commit-graph verify [--object-dir <objdir>] [--shallow]"),
        NULL
 };
 
@@ -25,7 +26,7 @@ static const char * const builtin_commit_graph_read_usage[] = {
 };
 
 static const char * const builtin_commit_graph_write_usage[] = {
-       N_("git commit-graph write [--object-dir <objdir>] [--append] [--reachable|--stdin-packs|--stdin-commits]"),
+       N_("git commit-graph write [--object-dir <objdir>] [--append|--split] [--reachable|--stdin-packs|--stdin-commits] <split options>"),
        NULL
 };
 
@@ -35,9 +36,10 @@ static struct opts_commit_graph {
        int stdin_packs;
        int stdin_commits;
        int append;
+       int split;
+       int shallow;
 } opts;
 
-
 static int graph_verify(int argc, const char **argv)
 {
        struct commit_graph *graph = NULL;
@@ -45,11 +47,14 @@ static int graph_verify(int argc, const char **argv)
        int open_ok;
        int fd;
        struct stat st;
+       int flags = 0;
 
        static struct option builtin_commit_graph_verify_options[] = {
                OPT_STRING(0, "object-dir", &opts.obj_dir,
                           N_("dir"),
                           N_("The object directory to store the graph")),
+               OPT_BOOL(0, "shallow", &opts.shallow,
+                        N_("if the commit-graph is split, only verify the tip file")),
                OPT_END(),
        };
 
@@ -59,21 +64,27 @@ static int graph_verify(int argc, const char **argv)
 
        if (!opts.obj_dir)
                opts.obj_dir = get_object_directory();
+       if (opts.shallow)
+               flags |= COMMIT_GRAPH_VERIFY_SHALLOW;
 
        graph_name = get_commit_graph_filename(opts.obj_dir);
        open_ok = open_commit_graph(graph_name, &fd, &st);
-       if (!open_ok && errno == ENOENT)
-               return 0;
-       if (!open_ok)
+       if (!open_ok && errno != ENOENT)
                die_errno(_("Could not open commit-graph '%s'"), graph_name);
-       graph = load_commit_graph_one_fd_st(fd, &st);
+
        FREE_AND_NULL(graph_name);
 
+       if (open_ok)
+               graph = load_commit_graph_one_fd_st(fd, &st);
+        else
+               graph = read_commit_graph_one(the_repository, opts.obj_dir);
+
+       /* Return failure if open_ok predicted success */
        if (!graph)
-               return 1;
+               return !!open_ok;
 
        UNLEAK(graph);
-       return verify_commit_graph(the_repository, graph);
+       return verify_commit_graph(the_repository, graph, flags);
 }
 
 static int graph_read(int argc, const char **argv)
@@ -135,6 +146,7 @@ static int graph_read(int argc, const char **argv)
 }
 
 extern int read_replace_refs;
+static struct split_commit_graph_opts split_opts;
 
 static int graph_write(int argc, const char **argv)
 {
@@ -156,9 +168,21 @@ static int graph_write(int argc, const char **argv)
                        N_("start walk at commits listed by stdin")),
                OPT_BOOL(0, "append", &opts.append,
                        N_("include all commits already in the commit-graph file")),
+               OPT_BOOL(0, "split", &opts.split,
+                       N_("allow writing an incremental commit-graph file")),
+               OPT_INTEGER(0, "max-commits", &split_opts.max_commits,
+                       N_("maximum number of commits in a non-base split commit-graph")),
+               OPT_INTEGER(0, "size-multiple", &split_opts.size_multiple,
+                       N_("maximum ratio between two levels of a split commit-graph")),
+               OPT_EXPIRY_DATE(0, "expire-time", &split_opts.expire_time,
+                       N_("maximum number of commits in a non-base split commit-graph")),
                OPT_END(),
        };
 
+       split_opts.size_multiple = 2;
+       split_opts.max_commits = 0;
+       split_opts.expire_time = 0;
+
        argc = parse_options(argc, argv, NULL,
                             builtin_commit_graph_write_options,
                             builtin_commit_graph_write_usage, 0);
@@ -169,11 +193,16 @@ static int graph_write(int argc, const char **argv)
                opts.obj_dir = get_object_directory();
        if (opts.append)
                flags |= COMMIT_GRAPH_APPEND;
+       if (opts.split)
+               flags |= COMMIT_GRAPH_SPLIT;
 
        read_replace_refs = 0;
 
-       if (opts.reachable)
-               return write_commit_graph_reachable(opts.obj_dir, flags);
+       if (opts.reachable) {
+               if (write_commit_graph_reachable(opts.obj_dir, flags, &split_opts))
+                       return 1;
+               return 0;
+       }
 
        string_list_init(&lines, 0);
        if (opts.stdin_packs || opts.stdin_commits) {
@@ -193,7 +222,8 @@ static int graph_write(int argc, const char **argv)
        if (write_commit_graph(opts.obj_dir,
                               pack_indexes,
                               commit_hex,
-                              flags))
+                              flags,
+                              &split_opts))
                result = 1;
 
        UNLEAK(lines);
index 3b561c2a75b5075ca688868ab965c7b96a87a9e8..3e4b5bfe4e1d18d8f7dfa8d33d0377b642653375 100644 (file)
@@ -1687,7 +1687,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
                      "not exceeded, and then \"git restore --staged :/\" to recover."));
 
        if (git_env_bool(GIT_TEST_COMMIT_GRAPH, 0) &&
-           write_commit_graph_reachable(get_object_directory(), 0))
+           write_commit_graph_reachable(get_object_directory(), 0, NULL))
                return 1;
 
        repo_rerere(the_repository, 0);
index be8e0bfcbe0a428f72c5762ff8a066baaefd2533..c18efadda53e54f0e80dbd16737e2d40f47fa16f 100644 (file)
@@ -687,7 +687,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 
        if (gc_write_commit_graph &&
            write_commit_graph_reachable(get_object_directory(),
-                                        !quiet && !daemonized ? COMMIT_GRAPH_PROGRESS : 0))
+                                        !quiet && !daemonized ? COMMIT_GRAPH_PROGRESS : 0,
+                                        NULL))
                return 1;
 
        if (auto_gc && too_many_loose_objects())
index 8cc1d1d6c3aff0842c42ecda5cc545c9eb085802..b3c4de79b6da4502726dbec5e21b11734d76e28e 100644 (file)
@@ -22,6 +22,7 @@
 #define GRAPH_CHUNKID_OIDLOOKUP 0x4f49444c /* "OIDL" */
 #define GRAPH_CHUNKID_DATA 0x43444154 /* "CDAT" */
 #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* "EDGE" */
+#define GRAPH_CHUNKID_BASE 0x42415345 /* "BASE" */
 
 #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)
 
 
 char *get_commit_graph_filename(const char *obj_dir)
 {
-       return xstrfmt("%s/info/commit-graph", obj_dir);
+       char *filename = xstrfmt("%s/info/commit-graph", obj_dir);
+       char *normalized = xmalloc(strlen(filename) + 1);
+       normalize_path_copy(normalized, filename);
+       free(filename);
+       return normalized;
+}
+
+static char *get_split_graph_filename(const char *obj_dir,
+                                     const char *oid_hex)
+{
+       char *filename = xstrfmt("%s/info/commit-graphs/graph-%s.graph",
+                                obj_dir,
+                                oid_hex);
+       char *normalized = xmalloc(strlen(filename) + 1);
+       normalize_path_copy(normalized, filename);
+       free(filename);
+       return normalized;
+}
+
+static char *get_chain_filename(const char *obj_dir)
+{
+       return xstrfmt("%s/info/commit-graphs/commit-graph-chain", obj_dir);
 }
 
 static uint8_t oid_version(void)
@@ -249,6 +271,12 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                        else
                                graph->chunk_extra_edges = data + chunk_offset;
                        break;
+
+               case GRAPH_CHUNKID_BASE:
+                       if (graph->chunk_base_graphs)
+                               chunk_repeated = 1;
+                       else
+                               graph->chunk_base_graphs = data + chunk_offset;
                }
 
                if (chunk_repeated) {
@@ -267,6 +295,8 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                last_chunk_offset = chunk_offset;
        }
 
+       hashcpy(graph->oid.hash, graph->data + graph->data_len - graph->hash_len);
+
        if (verify_commit_graph_lite(graph)) {
                free(graph);
                return NULL;
@@ -280,26 +310,151 @@ static struct commit_graph *load_commit_graph_one(const char *graph_file)
 
        struct stat st;
        int fd;
+       struct commit_graph *g;
        int open_ok = open_commit_graph(graph_file, &fd, &st);
 
        if (!open_ok)
                return NULL;
 
-       return load_commit_graph_one_fd_st(fd, &st);
+       g = load_commit_graph_one_fd_st(fd, &st);
+
+       if (g)
+               g->filename = xstrdup(graph_file);
+
+       return g;
+}
+
+static struct commit_graph *load_commit_graph_v1(struct repository *r, const char *obj_dir)
+{
+       char *graph_name = get_commit_graph_filename(obj_dir);
+       struct commit_graph *g = load_commit_graph_one(graph_name);
+       free(graph_name);
+
+       if (g)
+               g->obj_dir = obj_dir;
+
+       return g;
+}
+
+static int add_graph_to_chain(struct commit_graph *g,
+                             struct commit_graph *chain,
+                             struct object_id *oids,
+                             int n)
+{
+       struct commit_graph *cur_g = chain;
+
+       if (n && !g->chunk_base_graphs) {
+               warning(_("commit-graph has no base graphs chunk"));
+               return 0;
+       }
+
+       while (n) {
+               n--;
+
+               if (!cur_g ||
+                   !oideq(&oids[n], &cur_g->oid) ||
+                   !hasheq(oids[n].hash, g->chunk_base_graphs + g->hash_len * n)) {
+                       warning(_("commit-graph chain does not match"));
+                       return 0;
+               }
+
+               cur_g = cur_g->base_graph;
+       }
+
+       g->base_graph = chain;
+
+       if (chain)
+               g->num_commits_in_base = chain->num_commits + chain->num_commits_in_base;
+
+       return 1;
+}
+
+static struct commit_graph *load_commit_graph_chain(struct repository *r, const char *obj_dir)
+{
+       struct commit_graph *graph_chain = NULL;
+       struct strbuf line = STRBUF_INIT;
+       struct stat st;
+       struct object_id *oids;
+       int i = 0, valid = 1, count;
+       char *chain_name = get_chain_filename(obj_dir);
+       FILE *fp;
+       int stat_res;
+
+       fp = fopen(chain_name, "r");
+       stat_res = stat(chain_name, &st);
+       free(chain_name);
+
+       if (!fp ||
+           stat_res ||
+           st.st_size <= the_hash_algo->hexsz)
+               return NULL;
+
+       count = st.st_size / (the_hash_algo->hexsz + 1);
+       oids = xcalloc(count, sizeof(struct object_id));
+
+       prepare_alt_odb(r);
+
+       for (i = 0; i < count; i++) {
+               struct object_directory *odb;
+
+               if (strbuf_getline_lf(&line, fp) == EOF)
+                       break;
+
+               if (get_oid_hex(line.buf, &oids[i])) {
+                       warning(_("invalid commit-graph chain: line '%s' not a hash"),
+                               line.buf);
+                       valid = 0;
+                       break;
+               }
+
+               valid = 0;
+               for (odb = r->objects->odb; odb; odb = odb->next) {
+                       char *graph_name = get_split_graph_filename(odb->path, line.buf);
+                       struct commit_graph *g = load_commit_graph_one(graph_name);
+
+                       free(graph_name);
+
+                       if (g) {
+                               g->obj_dir = odb->path;
+
+                               if (add_graph_to_chain(g, graph_chain, oids, i)) {
+                                       graph_chain = g;
+                                       valid = 1;
+                               }
+
+                               break;
+                       }
+               }
+
+               if (!valid) {
+                       warning(_("unable to find all commit-graph files"));
+                       break;
+               }
+       }
+
+       free(oids);
+       fclose(fp);
+
+       return graph_chain;
+}
+
+struct commit_graph *read_commit_graph_one(struct repository *r, const char *obj_dir)
+{
+       struct commit_graph *g = load_commit_graph_v1(r, obj_dir);
+
+       if (!g)
+               g = load_commit_graph_chain(r, obj_dir);
+
+       return g;
 }
 
 static void prepare_commit_graph_one(struct repository *r, const char *obj_dir)
 {
-       char *graph_name;
 
        if (r->objects->commit_graph)
                return;
 
-       graph_name = get_commit_graph_filename(obj_dir);
-       r->objects->commit_graph =
-               load_commit_graph_one(graph_name);
-
-       FREE_AND_NULL(graph_name);
+       r->objects->commit_graph = read_commit_graph_one(r, obj_dir);
 }
 
 /*
@@ -361,9 +516,18 @@ int generation_numbers_enabled(struct repository *r)
        return !!first_generation;
 }
 
+static void close_commit_graph_one(struct commit_graph *g)
+{
+       if (!g)
+               return;
+
+       close_commit_graph_one(g->base_graph);
+       free_commit_graph(g);
+}
+
 void close_commit_graph(struct raw_object_store *o)
 {
-       free_commit_graph(o->commit_graph);
+       close_commit_graph_one(o->commit_graph);
        o->commit_graph = NULL;
 }
 
@@ -373,18 +537,38 @@ static int bsearch_graph(struct commit_graph *g, struct object_id *oid, uint32_t
                            g->chunk_oid_lookup, g->hash_len, pos);
 }
 
+static void load_oid_from_graph(struct commit_graph *g,
+                               uint32_t pos,
+                               struct object_id *oid)
+{
+       uint32_t lex_index;
+
+       while (g && pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       if (!g)
+               BUG("NULL commit-graph");
+
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die(_("invalid commit position. commit-graph is likely corrupt"));
+
+       lex_index = pos - g->num_commits_in_base;
+
+       hashcpy(oid->hash, g->chunk_oid_lookup + g->hash_len * lex_index);
+}
+
 static struct commit_list **insert_parent_or_die(struct repository *r,
                                                 struct commit_graph *g,
-                                                uint64_t pos,
+                                                uint32_t pos,
                                                 struct commit_list **pptr)
 {
        struct commit *c;
        struct object_id oid;
 
-       if (pos >= g->num_commits)
-               die("invalid parent position %"PRIu64, pos);
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die("invalid parent position %"PRIu32, pos);
 
-       hashcpy(oid.hash, g->chunk_oid_lookup + g->hash_len * pos);
+       load_oid_from_graph(g, pos, &oid);
        c = lookup_commit(r, &oid);
        if (!c)
                die(_("could not find commit %s"), oid_to_hex(&oid));
@@ -394,7 +578,14 @@ static struct commit_list **insert_parent_or_die(struct repository *r,
 
 static void fill_commit_graph_info(struct commit *item, struct commit_graph *g, uint32_t pos)
 {
-       const unsigned char *commit_data = g->chunk_commit_data + GRAPH_DATA_WIDTH * pos;
+       const unsigned char *commit_data;
+       uint32_t lex_index;
+
+       while (pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       lex_index = pos - g->num_commits_in_base;
+       commit_data = g->chunk_commit_data + GRAPH_DATA_WIDTH * lex_index;
        item->graph_pos = pos;
        item->generation = get_be32(commit_data + g->hash_len + 8) >> 2;
 }
@@ -412,10 +603,25 @@ static int fill_commit_in_graph(struct repository *r,
        uint32_t *parent_data_ptr;
        uint64_t date_low, date_high;
        struct commit_list **pptr;
-       const unsigned char *commit_data = g->chunk_commit_data + (g->hash_len + 16) * pos;
+       const unsigned char *commit_data;
+       uint32_t lex_index;
 
-       item->object.parsed = 1;
+       while (pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       if (pos >= g->num_commits + g->num_commits_in_base)
+               die(_("invalid commit position. commit-graph is likely corrupt"));
+
+       /*
+        * Store the "full" position, but then use the
+        * "local" position for the rest of the calculation.
+        */
        item->graph_pos = pos;
+       lex_index = pos - g->num_commits_in_base;
+
+       commit_data = g->chunk_commit_data + (g->hash_len + 16) * lex_index;
+
+       item->object.parsed = 1;
 
        set_commit_tree(item, NULL);
 
@@ -459,7 +665,18 @@ static int find_commit_in_graph(struct commit *item, struct commit_graph *g, uin
                *pos = item->graph_pos;
                return 1;
        } else {
-               return bsearch_graph(g, &(item->object.oid), pos);
+               struct commit_graph *cur_g = g;
+               uint32_t lex_index;
+
+               while (cur_g && !bsearch_graph(cur_g, &(item->object.oid), &lex_index))
+                       cur_g = cur_g->base_graph;
+
+               if (cur_g) {
+                       *pos = lex_index + cur_g->num_commits_in_base;
+                       return 1;
+               }
+
+               return 0;
        }
 }
 
@@ -499,8 +716,13 @@ static struct tree *load_tree_for_commit(struct repository *r,
                                         struct commit *c)
 {
        struct object_id oid;
-       const unsigned char *commit_data = g->chunk_commit_data +
-                                          GRAPH_DATA_WIDTH * (c->graph_pos);
+       const unsigned char *commit_data;
+
+       while (c->graph_pos < g->num_commits_in_base)
+               g = g->base_graph;
+
+       commit_data = g->chunk_commit_data +
+                       GRAPH_DATA_WIDTH * (c->graph_pos - g->num_commits_in_base);
 
        hashcpy(oid.hash, commit_data);
        set_commit_tree(c, lookup_tree(r, &oid));
@@ -539,7 +761,7 @@ struct packed_oid_list {
 
 struct write_commit_graph_context {
        struct repository *r;
-       const char *obj_dir;
+       char *obj_dir;
        char *graph_name;
        struct packed_oid_list oids;
        struct packed_commit_list commits;
@@ -548,8 +770,21 @@ struct write_commit_graph_context {
        struct progress *progress;
        int progress_done;
        uint64_t progress_cnt;
+
+       char *base_graph_name;
+       int num_commit_graphs_before;
+       int num_commit_graphs_after;
+       char **commit_graph_filenames_before;
+       char **commit_graph_filenames_after;
+       char **commit_graph_hash_after;
+       uint32_t new_num_commits_in_base;
+       struct commit_graph *new_base_graph;
+
        unsigned append:1,
-                report_progress:1;
+                report_progress:1,
+                split:1;
+
+       const struct split_commit_graph_opts *split_opts;
 };
 
 static void write_graph_chunk_fanout(struct hashfile *f,
@@ -619,6 +854,16 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,
                                              ctx->commits.nr,
                                              commit_to_sha1);
 
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -639,6 +884,17 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,
                                              ctx->commits.list,
                                              ctx->commits.nr,
                                              commit_to_sha1);
+
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -696,6 +952,16 @@ static void write_graph_chunk_extra_edges(struct hashfile *f,
                                                  ctx->commits.nr,
                                                  commit_to_sha1);
 
+                       if (edge_value >= 0)
+                               edge_value += ctx->new_num_commits_in_base;
+                       else {
+                               uint32_t pos;
+                               if (find_commit_in_graph(parent->item,
+                                                        ctx->new_base_graph,
+                                                        &pos))
+                                       edge_value = pos;
+                       }
+
                        if (edge_value < 0)
                                BUG("missing parent %s for commit %s",
                                    oid_to_hex(&parent->item->object.oid),
@@ -710,7 +976,7 @@ static void write_graph_chunk_extra_edges(struct hashfile *f,
        }
 }
 
-static int commit_compare(const void *_a, const void *_b)
+static int oid_compare(const void *_a, const void *_b)
 {
        const struct object_id *a = (const struct object_id *)_a;
        const struct object_id *b = (const struct object_id *)_b;
@@ -787,7 +1053,13 @@ static void close_reachable(struct write_commit_graph_context *ctx)
                display_progress(ctx->progress, i + 1);
                commit = lookup_commit(ctx->r, &ctx->oids.list[i]);
 
-               if (commit && !parse_commit_no_graph(commit))
+               if (!commit)
+                       continue;
+               if (ctx->split) {
+                       if (!parse_commit(commit) &&
+                           commit->graph_pos == COMMIT_NOT_FROM_GRAPH)
+                               add_missing_parents(ctx, commit);
+               } else if (!parse_commit_no_graph(commit))
                        add_missing_parents(ctx, commit);
        }
        stop_progress(&ctx->progress);
@@ -861,14 +1133,15 @@ static int add_ref_to_list(const char *refname,
        return 0;
 }
 
-int write_commit_graph_reachable(const char *obj_dir, unsigned int flags)
+int write_commit_graph_reachable(const char *obj_dir, unsigned int flags,
+                                const struct split_commit_graph_opts *split_opts)
 {
        struct string_list list = STRING_LIST_INIT_DUP;
        int result;
 
        for_each_ref(add_ref_to_list, &list);
        result = write_commit_graph(obj_dir, NULL, &list,
-                                   flags);
+                                   flags, split_opts);
 
        string_list_clear(&list, 0);
        return result;
@@ -979,12 +1252,20 @@ static uint32_t count_distinct_commits(struct write_commit_graph_context *ctx)
                        _("Counting distinct commits in commit graph"),
                        ctx->oids.nr);
        display_progress(ctx->progress, 0); /* TODO: Measure QSORT() progress */
-       QSORT(ctx->oids.list, ctx->oids.nr, commit_compare);
+       QSORT(ctx->oids.list, ctx->oids.nr, oid_compare);
 
        for (i = 1; i < ctx->oids.nr; i++) {
                display_progress(ctx->progress, i + 1);
-               if (!oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i]))
+               if (!oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i])) {
+                       if (ctx->split) {
+                               struct commit *c = lookup_commit(ctx->r, &ctx->oids.list[i]);
+
+                               if (!c || c->graph_pos != COMMIT_NOT_FROM_GRAPH)
+                                       continue;
+                       }
+
                        count_distinct++;
+               }
        }
        stop_progress(&ctx->progress);
 
@@ -1007,7 +1288,13 @@ static void copy_oids_to_commits(struct write_commit_graph_context *ctx)
                if (i > 0 && oideq(&ctx->oids.list[i - 1], &ctx->oids.list[i]))
                        continue;
 
+               ALLOC_GROW(ctx->commits.list, ctx->commits.nr + 1, ctx->commits.alloc);
                ctx->commits.list[ctx->commits.nr] = lookup_commit(ctx->r, &ctx->oids.list[i]);
+
+               if (ctx->split &&
+                   ctx->commits.list[ctx->commits.nr]->graph_pos != COMMIT_NOT_FROM_GRAPH)
+                       continue;
+
                parse_commit_no_graph(ctx->commits.list[ctx->commits.nr]);
 
                for (parent = ctx->commits.list[ctx->commits.nr]->parents;
@@ -1022,18 +1309,56 @@ static void copy_oids_to_commits(struct write_commit_graph_context *ctx)
        stop_progress(&ctx->progress);
 }
 
+static int write_graph_chunk_base_1(struct hashfile *f,
+                                   struct commit_graph *g)
+{
+       int num = 0;
+
+       if (!g)
+               return 0;
+
+       num = write_graph_chunk_base_1(f, g->base_graph);
+       hashwrite(f, g->oid.hash, the_hash_algo->rawsz);
+       return num + 1;
+}
+
+static int write_graph_chunk_base(struct hashfile *f,
+                                 struct write_commit_graph_context *ctx)
+{
+       int num = write_graph_chunk_base_1(f, ctx->new_base_graph);
+
+       if (num != ctx->num_commit_graphs_after - 1) {
+               error(_("failed to write correct number of base graph ids"));
+               return -1;
+       }
+
+       return 0;
+}
+
 static int write_commit_graph_file(struct write_commit_graph_context *ctx)
 {
        uint32_t i;
+       int fd;
        struct hashfile *f;
        struct lock_file lk = LOCK_INIT;
-       uint32_t chunk_ids[5];
-       uint64_t chunk_offsets[5];
+       uint32_t chunk_ids[6];
+       uint64_t chunk_offsets[6];
        const unsigned hashsz = the_hash_algo->rawsz;
        struct strbuf progress_title = STRBUF_INIT;
-       int num_chunks = ctx->num_extra_edges ? 4 : 3;
+       int num_chunks = 3;
+       struct object_id file_hash;
+
+       if (ctx->split) {
+               struct strbuf tmp_file = STRBUF_INIT;
+
+               strbuf_addf(&tmp_file,
+                           "%s/info/commit-graphs/tmp_graph_XXXXXX",
+                           ctx->obj_dir);
+               ctx->graph_name = strbuf_detach(&tmp_file, NULL);
+       } else {
+               ctx->graph_name = get_commit_graph_filename(ctx->obj_dir);
+       }
 
-       ctx->graph_name = get_commit_graph_filename(ctx->obj_dir);
        if (safe_create_leading_directories(ctx->graph_name)) {
                UNLEAK(ctx->graph_name);
                error(_("unable to create leading directories of %s"),
@@ -1041,30 +1366,61 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
                return -1;
        }
 
-       hold_lock_file_for_update(&lk, ctx->graph_name, LOCK_DIE_ON_ERROR);
-       f = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);
+       if (ctx->split) {
+               char *lock_name = get_chain_filename(ctx->obj_dir);
 
-       hashwrite_be32(f, GRAPH_SIGNATURE);
+               hold_lock_file_for_update(&lk, lock_name, LOCK_DIE_ON_ERROR);
 
-       hashwrite_u8(f, GRAPH_VERSION);
-       hashwrite_u8(f, oid_version());
-       hashwrite_u8(f, num_chunks);
-       hashwrite_u8(f, 0); /* unused padding byte */
+               fd = git_mkstemp_mode(ctx->graph_name, 0444);
+               if (fd < 0) {
+                       error(_("unable to create '%s'"), ctx->graph_name);
+                       return -1;
+               }
+
+               f = hashfd(fd, ctx->graph_name);
+       } else {
+               hold_lock_file_for_update(&lk, ctx->graph_name, LOCK_DIE_ON_ERROR);
+               fd = lk.tempfile->fd;
+               f = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);
+       }
 
        chunk_ids[0] = GRAPH_CHUNKID_OIDFANOUT;
        chunk_ids[1] = GRAPH_CHUNKID_OIDLOOKUP;
        chunk_ids[2] = GRAPH_CHUNKID_DATA;
-       if (ctx->num_extra_edges)
-               chunk_ids[3] = GRAPH_CHUNKID_EXTRAEDGES;
-       else
-               chunk_ids[3] = 0;
-       chunk_ids[4] = 0;
+       if (ctx->num_extra_edges) {
+               chunk_ids[num_chunks] = GRAPH_CHUNKID_EXTRAEDGES;
+               num_chunks++;
+       }
+       if (ctx->num_commit_graphs_after > 1) {
+               chunk_ids[num_chunks] = GRAPH_CHUNKID_BASE;
+               num_chunks++;
+       }
+
+       chunk_ids[num_chunks] = 0;
 
        chunk_offsets[0] = 8 + (num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH;
        chunk_offsets[1] = chunk_offsets[0] + GRAPH_FANOUT_SIZE;
        chunk_offsets[2] = chunk_offsets[1] + hashsz * ctx->commits.nr;
        chunk_offsets[3] = chunk_offsets[2] + (hashsz + 16) * ctx->commits.nr;
-       chunk_offsets[4] = chunk_offsets[3] + 4 * ctx->num_extra_edges;
+
+       num_chunks = 3;
+       if (ctx->num_extra_edges) {
+               chunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +
+                                               4 * ctx->num_extra_edges;
+               num_chunks++;
+       }
+       if (ctx->num_commit_graphs_after > 1) {
+               chunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +
+                                               hashsz * (ctx->num_commit_graphs_after - 1);
+               num_chunks++;
+       }
+
+       hashwrite_be32(f, GRAPH_SIGNATURE);
+
+       hashwrite_u8(f, GRAPH_VERSION);
+       hashwrite_u8(f, oid_version());
+       hashwrite_u8(f, num_chunks);
+       hashwrite_u8(f, ctx->num_commit_graphs_after - 1);
 
        for (i = 0; i <= num_chunks; i++) {
                uint32_t chunk_write[3];
@@ -1090,23 +1446,316 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)
        write_graph_chunk_data(f, hashsz, ctx);
        if (ctx->num_extra_edges)
                write_graph_chunk_extra_edges(f, ctx);
+       if (ctx->num_commit_graphs_after > 1 &&
+           write_graph_chunk_base(f, ctx)) {
+               return -1;
+       }
        stop_progress(&ctx->progress);
        strbuf_release(&progress_title);
 
+       if (ctx->split && ctx->base_graph_name && ctx->num_commit_graphs_after > 1) {
+               char *new_base_hash = xstrdup(oid_to_hex(&ctx->new_base_graph->oid));
+               char *new_base_name = get_split_graph_filename(ctx->new_base_graph->obj_dir, new_base_hash);
+
+               free(ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 2]);
+               free(ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 2]);
+               ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 2] = new_base_name;
+               ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 2] = new_base_hash;
+       }
+
        close_commit_graph(ctx->r->objects);
-       finalize_hashfile(f, NULL, CSUM_HASH_IN_STREAM | CSUM_FSYNC);
+       finalize_hashfile(f, file_hash.hash, CSUM_HASH_IN_STREAM | CSUM_FSYNC);
+
+       if (ctx->split) {
+               FILE *chainf = fdopen_lock_file(&lk, "w");
+               char *final_graph_name;
+               int result;
+
+               close(fd);
+
+               if (!chainf) {
+                       error(_("unable to open commit-graph chain file"));
+                       return -1;
+               }
+
+               if (ctx->base_graph_name) {
+                       const char *dest = ctx->commit_graph_filenames_after[
+                                               ctx->num_commit_graphs_after - 2];
+
+                       if (strcmp(ctx->base_graph_name, dest)) {
+                               result = rename(ctx->base_graph_name, dest);
+
+                               if (result) {
+                                       error(_("failed to rename base commit-graph file"));
+                                       return -1;
+                               }
+                       }
+               } else {
+                       char *graph_name = get_commit_graph_filename(ctx->obj_dir);
+                       unlink(graph_name);
+               }
+
+               ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1] = xstrdup(oid_to_hex(&file_hash));
+               final_graph_name = get_split_graph_filename(ctx->obj_dir,
+                                       ctx->commit_graph_hash_after[ctx->num_commit_graphs_after - 1]);
+               ctx->commit_graph_filenames_after[ctx->num_commit_graphs_after - 1] = final_graph_name;
+
+               result = rename(ctx->graph_name, final_graph_name);
+
+               for (i = 0; i < ctx->num_commit_graphs_after; i++)
+                       fprintf(lk.tempfile->fp, "%s\n", ctx->commit_graph_hash_after[i]);
+
+               if (result) {
+                       error(_("failed to rename temporary commit-graph file"));
+                       return -1;
+               }
+       }
+
        commit_lock_file(&lk);
 
        return 0;
 }
 
+static void split_graph_merge_strategy(struct write_commit_graph_context *ctx)
+{
+       struct commit_graph *g = ctx->r->objects->commit_graph;
+       uint32_t num_commits = ctx->commits.nr;
+       uint32_t i;
+
+       int max_commits = 0;
+       int size_mult = 2;
+
+       if (ctx->split_opts) {
+               max_commits = ctx->split_opts->max_commits;
+               size_mult = ctx->split_opts->size_multiple;
+       }
+
+       g = ctx->r->objects->commit_graph;
+       ctx->num_commit_graphs_after = ctx->num_commit_graphs_before + 1;
+
+       while (g && (g->num_commits <= size_mult * num_commits ||
+                   (max_commits && num_commits > max_commits))) {
+               if (strcmp(g->obj_dir, ctx->obj_dir))
+                       break;
+
+               num_commits += g->num_commits;
+               g = g->base_graph;
+
+               ctx->num_commit_graphs_after--;
+       }
+
+       ctx->new_base_graph = g;
+
+       if (ctx->num_commit_graphs_after == 2) {
+               char *old_graph_name = get_commit_graph_filename(g->obj_dir);
+
+               if (!strcmp(g->filename, old_graph_name) &&
+                   strcmp(g->obj_dir, ctx->obj_dir)) {
+                       ctx->num_commit_graphs_after = 1;
+                       ctx->new_base_graph = NULL;
+               }
+
+               free(old_graph_name);
+       }
+
+       ALLOC_ARRAY(ctx->commit_graph_filenames_after, ctx->num_commit_graphs_after);
+       ALLOC_ARRAY(ctx->commit_graph_hash_after, ctx->num_commit_graphs_after);
+
+       for (i = 0; i < ctx->num_commit_graphs_after &&
+                   i < ctx->num_commit_graphs_before; i++)
+               ctx->commit_graph_filenames_after[i] = xstrdup(ctx->commit_graph_filenames_before[i]);
+
+       i = ctx->num_commit_graphs_before - 1;
+       g = ctx->r->objects->commit_graph;
+
+       while (g) {
+               if (i < ctx->num_commit_graphs_after)
+                       ctx->commit_graph_hash_after[i] = xstrdup(oid_to_hex(&g->oid));
+
+               i--;
+               g = g->base_graph;
+       }
+}
+
+static void merge_commit_graph(struct write_commit_graph_context *ctx,
+                              struct commit_graph *g)
+{
+       uint32_t i;
+       uint32_t offset = g->num_commits_in_base;
+
+       ALLOC_GROW(ctx->commits.list, ctx->commits.nr + g->num_commits, ctx->commits.alloc);
+
+       for (i = 0; i < g->num_commits; i++) {
+               struct object_id oid;
+               struct commit *result;
+
+               display_progress(ctx->progress, i + 1);
+
+               load_oid_from_graph(g, i + offset, &oid);
+
+               /* only add commits if they still exist in the repo */
+               result = lookup_commit_reference_gently(ctx->r, &oid, 1);
+
+               if (result) {
+                       ctx->commits.list[ctx->commits.nr] = result;
+                       ctx->commits.nr++;
+               }
+       }
+}
+
+static int commit_compare(const void *_a, const void *_b)
+{
+       const struct commit *a = *(const struct commit **)_a;
+       const struct commit *b = *(const struct commit **)_b;
+       return oidcmp(&a->object.oid, &b->object.oid);
+}
+
+static void sort_and_scan_merged_commits(struct write_commit_graph_context *ctx)
+{
+       uint32_t i, num_parents;
+       struct commit_list *parent;
+
+       if (ctx->report_progress)
+               ctx->progress = start_delayed_progress(
+                                       _("Scanning merged commits"),
+                                       ctx->commits.nr);
+
+       QSORT(ctx->commits.list, ctx->commits.nr, commit_compare);
+
+       ctx->num_extra_edges = 0;
+       for (i = 0; i < ctx->commits.nr; i++) {
+               display_progress(ctx->progress, i);
+
+               if (i && oideq(&ctx->commits.list[i - 1]->object.oid,
+                         &ctx->commits.list[i]->object.oid)) {
+                       die(_("unexpected duplicate commit id %s"),
+                           oid_to_hex(&ctx->commits.list[i]->object.oid));
+               } else {
+                       num_parents = 0;
+                       for (parent = ctx->commits.list[i]->parents; parent; parent = parent->next)
+                               num_parents++;
+
+                       if (num_parents > 2)
+                               ctx->num_extra_edges += num_parents - 2;
+               }
+       }
+
+       stop_progress(&ctx->progress);
+}
+
+static void merge_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       struct commit_graph *g = ctx->r->objects->commit_graph;
+       uint32_t current_graph_number = ctx->num_commit_graphs_before;
+       struct strbuf progress_title = STRBUF_INIT;
+
+       while (g && current_graph_number >= ctx->num_commit_graphs_after) {
+               current_graph_number--;
+
+               if (ctx->report_progress) {
+                       strbuf_addstr(&progress_title, _("Merging commit-graph"));
+                       ctx->progress = start_delayed_progress(progress_title.buf, 0);
+               }
+
+               merge_commit_graph(ctx, g);
+               stop_progress(&ctx->progress);
+               strbuf_release(&progress_title);
+
+               g = g->base_graph;
+       }
+
+       if (g) {
+               ctx->new_base_graph = g;
+               ctx->new_num_commits_in_base = g->num_commits + g->num_commits_in_base;
+       }
+
+       if (ctx->new_base_graph)
+               ctx->base_graph_name = xstrdup(ctx->new_base_graph->filename);
+
+       sort_and_scan_merged_commits(ctx);
+}
+
+static void mark_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       uint32_t i;
+       time_t now = time(NULL);
+
+       for (i = ctx->num_commit_graphs_after - 1; i < ctx->num_commit_graphs_before; i++) {
+               struct stat st;
+               struct utimbuf updated_time;
+
+               stat(ctx->commit_graph_filenames_before[i], &st);
+
+               updated_time.actime = st.st_atime;
+               updated_time.modtime = now;
+               utime(ctx->commit_graph_filenames_before[i], &updated_time);
+       }
+}
+
+static void expire_commit_graphs(struct write_commit_graph_context *ctx)
+{
+       struct strbuf path = STRBUF_INIT;
+       DIR *dir;
+       struct dirent *de;
+       size_t dirnamelen;
+       timestamp_t expire_time = time(NULL);
+
+       if (ctx->split_opts && ctx->split_opts->expire_time)
+               expire_time -= ctx->split_opts->expire_time;
+       if (!ctx->split) {
+               char *chain_file_name = get_chain_filename(ctx->obj_dir);
+               unlink(chain_file_name);
+               free(chain_file_name);
+               ctx->num_commit_graphs_after = 0;
+       }
+
+       strbuf_addstr(&path, ctx->obj_dir);
+       strbuf_addstr(&path, "/info/commit-graphs");
+       dir = opendir(path.buf);
+
+       if (!dir) {
+               strbuf_release(&path);
+               return;
+       }
+
+       strbuf_addch(&path, '/');
+       dirnamelen = path.len;
+       while ((de = readdir(dir)) != NULL) {
+               struct stat st;
+               uint32_t i, found = 0;
+
+               strbuf_setlen(&path, dirnamelen);
+               strbuf_addstr(&path, de->d_name);
+
+               stat(path.buf, &st);
+
+               if (st.st_mtime > expire_time)
+                       continue;
+               if (path.len < 6 || strcmp(path.buf + path.len - 6, ".graph"))
+                       continue;
+
+               for (i = 0; i < ctx->num_commit_graphs_after; i++) {
+                       if (!strcmp(ctx->commit_graph_filenames_after[i],
+                                   path.buf)) {
+                               found = 1;
+                               break;
+                       }
+               }
+
+               if (!found)
+                       unlink(path.buf);
+       }
+}
+
 int write_commit_graph(const char *obj_dir,
                       struct string_list *pack_indexes,
                       struct string_list *commit_hex,
-                      unsigned int flags)
+                      unsigned int flags,
+                      const struct split_commit_graph_opts *split_opts)
 {
        struct write_commit_graph_context *ctx;
        uint32_t i, count_distinct = 0;
+       size_t len;
        int res = 0;
 
        if (!commit_graph_compatible(the_repository))
@@ -1114,13 +1763,48 @@ int write_commit_graph(const char *obj_dir,
 
        ctx = xcalloc(1, sizeof(struct write_commit_graph_context));
        ctx->r = the_repository;
-       ctx->obj_dir = obj_dir;
+
+       /* normalize object dir with no trailing slash */
+       ctx->obj_dir = xmallocz(strlen(obj_dir) + 1);
+       normalize_path_copy(ctx->obj_dir, obj_dir);
+       len = strlen(ctx->obj_dir);
+       if (len && ctx->obj_dir[len - 1] == '/')
+               ctx->obj_dir[len - 1] = 0;
+
        ctx->append = flags & COMMIT_GRAPH_APPEND ? 1 : 0;
        ctx->report_progress = flags & COMMIT_GRAPH_PROGRESS ? 1 : 0;
+       ctx->split = flags & COMMIT_GRAPH_SPLIT ? 1 : 0;
+       ctx->split_opts = split_opts;
+
+       if (ctx->split) {
+               struct commit_graph *g;
+               prepare_commit_graph(ctx->r);
+
+               g = ctx->r->objects->commit_graph;
+
+               while (g) {
+                       ctx->num_commit_graphs_before++;
+                       g = g->base_graph;
+               }
+
+               if (ctx->num_commit_graphs_before) {
+                       ALLOC_ARRAY(ctx->commit_graph_filenames_before, ctx->num_commit_graphs_before);
+                       i = ctx->num_commit_graphs_before;
+                       g = ctx->r->objects->commit_graph;
+
+                       while (g) {
+                               ctx->commit_graph_filenames_before[--i] = xstrdup(g->filename);
+                               g = g->base_graph;
+                       }
+               }
+       }
 
        ctx->approx_nr_objects = approximate_object_count();
        ctx->oids.alloc = ctx->approx_nr_objects / 32;
 
+       if (ctx->split && split_opts && ctx->oids.alloc > split_opts->max_commits)
+               ctx->oids.alloc = split_opts->max_commits;
+
        if (ctx->append) {
                prepare_commit_graph_one(ctx->r, ctx->obj_dir);
                if (ctx->r->objects->commit_graph)
@@ -1171,14 +1855,45 @@ int write_commit_graph(const char *obj_dir,
                goto cleanup;
        }
 
+       if (!ctx->commits.nr)
+               goto cleanup;
+
+       if (ctx->split) {
+               split_graph_merge_strategy(ctx);
+
+               merge_commit_graphs(ctx);
+       } else
+               ctx->num_commit_graphs_after = 1;
+
        compute_generation_numbers(ctx);
 
        res = write_commit_graph_file(ctx);
 
+       if (ctx->split)
+               mark_commit_graphs(ctx);
+
+       expire_commit_graphs(ctx);
+
 cleanup:
        free(ctx->graph_name);
        free(ctx->commits.list);
        free(ctx->oids.list);
+       free(ctx->obj_dir);
+
+       if (ctx->commit_graph_filenames_after) {
+               for (i = 0; i < ctx->num_commit_graphs_after; i++) {
+                       free(ctx->commit_graph_filenames_after[i]);
+                       free(ctx->commit_graph_hash_after[i]);
+               }
+
+               for (i = 0; i < ctx->num_commit_graphs_before; i++)
+                       free(ctx->commit_graph_filenames_before[i]);
+
+               free(ctx->commit_graph_filenames_after);
+               free(ctx->commit_graph_filenames_before);
+               free(ctx->commit_graph_hash_after);
+       }
+
        free(ctx);
 
        return res;
@@ -1201,7 +1916,7 @@ static void graph_report(const char *fmt, ...)
 #define GENERATION_ZERO_EXISTS 1
 #define GENERATION_NUMBER_EXISTS 2
 
-int verify_commit_graph(struct repository *r, struct commit_graph *g)
+int verify_commit_graph(struct repository *r, struct commit_graph *g, int flags)
 {
        uint32_t i, cur_fanout_pos = 0;
        struct object_id prev_oid, cur_oid, checksum;
@@ -1209,6 +1924,7 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
        struct hashfile *f;
        int devnull;
        struct progress *progress = NULL;
+       int local_error = 0;
 
        if (!g) {
                graph_report("no commit-graph file loaded");
@@ -1303,6 +2019,9 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
                                break;
                        }
 
+                       /* parse parent in case it is in a base graph */
+                       parse_commit_in_graph_one(r, g, graph_parents->item);
+
                        if (!oideq(&graph_parents->item->object.oid, &odb_parents->item->object.oid))
                                graph_report(_("commit-graph parent for %s is %s != %s"),
                                             oid_to_hex(&cur_oid),
@@ -1354,7 +2073,12 @@ int verify_commit_graph(struct repository *r, struct commit_graph *g)
        }
        stop_progress(&progress);
 
-       return verify_commit_graph_error;
+       local_error = verify_commit_graph_error;
+
+       if (!(flags & COMMIT_GRAPH_VERIFY_SHALLOW) && g->base_graph)
+               local_error |= verify_commit_graph(r, g->base_graph, flags);
+
+       return local_error;
 }
 
 void free_commit_graph(struct commit_graph *g)
@@ -1366,5 +2090,6 @@ void free_commit_graph(struct commit_graph *g)
                g->data = NULL;
                close(g->graph_fd);
        }
+       free(g->filename);
        free(g);
 }
index 390c7f696104fbe772151b91cb04889d0f682401..df9a3b20e4abc7d388acab1cc85546aafa8345a3 100644 (file)
@@ -47,15 +47,21 @@ struct commit_graph {
        unsigned char num_chunks;
        uint32_t num_commits;
        struct object_id oid;
+       char *filename;
+       const char *obj_dir;
+
+       uint32_t num_commits_in_base;
+       struct commit_graph *base_graph;
 
        const uint32_t *chunk_oid_fanout;
        const unsigned char *chunk_oid_lookup;
        const unsigned char *chunk_commit_data;
        const unsigned char *chunk_extra_edges;
+       const unsigned char *chunk_base_graphs;
 };
 
 struct commit_graph *load_commit_graph_one_fd_st(int fd, struct stat *st);
-
+struct commit_graph *read_commit_graph_one(struct repository *r, const char *obj_dir);
 struct commit_graph *parse_commit_graph(void *graph_map, int fd,
                                        size_t graph_size);
 
@@ -67,6 +73,13 @@ int generation_numbers_enabled(struct repository *r);
 
 #define COMMIT_GRAPH_APPEND     (1 << 0)
 #define COMMIT_GRAPH_PROGRESS   (1 << 1)
+#define COMMIT_GRAPH_SPLIT      (1 << 2)
+
+struct split_commit_graph_opts {
+       int size_multiple;
+       int max_commits;
+       timestamp_t expire_time;
+};
 
 /*
  * The write_commit_graph* methods return zero on success
@@ -74,13 +87,17 @@ int generation_numbers_enabled(struct repository *r);
  * is not compatible with the commit-graph feature, then the
  * methods will return 0 without writing a commit-graph.
  */
-int write_commit_graph_reachable(const char *obj_dir, unsigned int flags);
+int write_commit_graph_reachable(const char *obj_dir, unsigned int flags,
+                                const struct split_commit_graph_opts *split_opts);
 int write_commit_graph(const char *obj_dir,
                       struct string_list *pack_indexes,
                       struct string_list *commit_hex,
-                      unsigned int flags);
+                      unsigned int flags,
+                      const struct split_commit_graph_opts *split_opts);
+
+#define COMMIT_GRAPH_VERIFY_SHALLOW    (1 << 0)
 
-int verify_commit_graph(struct repository *r, struct commit_graph *g);
+int verify_commit_graph(struct repository *r, struct commit_graph *g, int flags);
 
 void close_commit_graph(struct raw_object_store *);
 void free_commit_graph(struct commit_graph *);
index 5267c4be20e709bb1632c5b9786ffdb418bcb5c9..22cb9d66430410f726e821e906fa79587f43c3e8 100755 (executable)
@@ -20,7 +20,7 @@ test_expect_success 'verify graph with no graph file' '
 test_expect_success 'write graph with no packs' '
        cd "$TRASH_DIRECTORY/full" &&
        git commit-graph write --object-dir . &&
-       test_path_is_file info/commit-graph
+       test_path_is_missing info/commit-graph
 '
 
 test_expect_success 'close with correct error on bad input' '
diff --git a/t/t5324-split-commit-graph.sh b/t/t5324-split-commit-graph.sh
new file mode 100755 (executable)
index 0000000..03f45a1
--- /dev/null
@@ -0,0 +1,343 @@
+#!/bin/sh
+
+test_description='split commit graph'
+. ./test-lib.sh
+
+GIT_TEST_COMMIT_GRAPH=0
+
+test_expect_success 'setup repo' '
+       git init &&
+       git config core.commitGraph true &&
+       infodir=".git/objects/info" &&
+       graphdir="$infodir/commit-graphs" &&
+       test_oid_init
+'
+
+graph_read_expect() {
+       NUM_BASE=0
+       if test ! -z $2
+       then
+               NUM_BASE=$2
+       fi
+       cat >expect <<- EOF
+       header: 43475048 1 1 3 $NUM_BASE
+       num_commits: $1
+       chunks: oid_fanout oid_lookup commit_metadata
+       EOF
+       git commit-graph read >output &&
+       test_cmp expect output
+}
+
+test_expect_success 'create commits and write commit-graph' '
+       for i in $(test_seq 3)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git commit-graph write --reachable &&
+       test_path_is_file $infodir/commit-graph &&
+       graph_read_expect 3
+'
+
+graph_git_two_modes() {
+       git -c core.commitGraph=true $1 >output
+       git -c core.commitGraph=false $1 >expect
+       test_cmp expect output
+}
+
+graph_git_behavior() {
+       MSG=$1
+       BRANCH=$2
+       COMPARE=$3
+       test_expect_success "check normal git operations: $MSG" '
+               graph_git_two_modes "log --oneline $BRANCH" &&
+               graph_git_two_modes "log --topo-order $BRANCH" &&
+               graph_git_two_modes "log --graph $COMPARE..$BRANCH" &&
+               graph_git_two_modes "branch -vv" &&
+               graph_git_two_modes "merge-base -a $BRANCH $COMPARE"
+       '
+}
+
+graph_git_behavior 'graph exists' commits/3 commits/1
+
+verify_chain_files_exist() {
+       for hash in $(cat $1/commit-graph-chain)
+       do
+               test_path_is_file $1/graph-$hash.graph || return 1
+       done
+}
+
+test_expect_success 'add more commits, and write a new base graph' '
+       git reset --hard commits/1 &&
+       for i in $(test_seq 4 5)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git reset --hard commits/2 &&
+       for i in $(test_seq 6 10)
+       do
+               test_commit $i &&
+               git branch commits/$i || return 1
+       done &&
+       git reset --hard commits/2 &&
+       git merge commits/4 &&
+       git branch merge/1 &&
+       git reset --hard commits/4 &&
+       git merge commits/6 &&
+       git branch merge/2 &&
+       git commit-graph write --reachable &&
+       graph_read_expect 12
+'
+
+test_expect_success 'fork and fail to base a chain on a commit-graph file' '
+       test_when_finished rm -rf fork &&
+       git clone . fork &&
+       (
+               cd fork &&
+               rm .git/objects/info/commit-graph &&
+               echo "$(pwd)/../.git/objects" >.git/objects/info/alternates &&
+               test_commit new-commit &&
+               git commit-graph write --reachable --split &&
+               test_path_is_file $graphdir/commit-graph-chain &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               verify_chain_files_exist $graphdir
+       )
+'
+
+test_expect_success 'add three more commits, write a tip graph' '
+       git reset --hard commits/3 &&
+       git merge merge/1 &&
+       git merge commits/5 &&
+       git merge merge/2 &&
+       git branch merge/3 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_missing $infodir/commit-graph &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 2 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'split commit-graph: merge 3 vs 2' merge/3 merge/2
+
+test_expect_success 'add one commit, write a tip graph' '
+       test_commit 11 &&
+       git branch commits/11 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_missing $infodir/commit-graph &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 3 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'three-layer commit-graph: commit 11 vs 6' commits/11 commits/6
+
+test_expect_success 'add one commit, write a merged graph' '
+       test_commit 12 &&
+       git branch commits/12 &&
+       git commit-graph write --reachable --split &&
+       test_path_is_file $graphdir/commit-graph-chain &&
+       test_line_count = 2 $graphdir/commit-graph-chain &&
+       ls $graphdir/graph-*.graph >graph-files &&
+       test_line_count = 2 graph-files &&
+       verify_chain_files_exist $graphdir
+'
+
+graph_git_behavior 'merged commit-graph: commit 12 vs 6' commits/12 commits/6
+
+test_expect_success 'create fork and chain across alternate' '
+       git clone . fork &&
+       (
+               cd fork &&
+               git config core.commitGraph true &&
+               rm -rf $graphdir &&
+               echo "$(pwd)/../.git/objects" >.git/objects/info/alternates &&
+               test_commit 13 &&
+               git branch commits/13 &&
+               git commit-graph write --reachable --split &&
+               test_path_is_file $graphdir/commit-graph-chain &&
+               test_line_count = 3 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files &&
+               git -c core.commitGraph=true  rev-list HEAD >expect &&
+               git -c core.commitGraph=false rev-list HEAD >actual &&
+               test_cmp expect actual &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --object-dir=.git/objects/ &&
+               test_line_count = 3 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       )
+'
+
+graph_git_behavior 'alternate: commit 13 vs 6' commits/13 commits/6
+
+test_expect_success 'test merge stragety constants' '
+       git clone . merge-2 &&
+       (
+               cd merge-2 &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --size-multiple=2 &&
+               test_line_count = 3 $graphdir/commit-graph-chain
+
+       ) &&
+       git clone . merge-10 &&
+       (
+               cd merge-10 &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 14 &&
+               git commit-graph write --reachable --split --size-multiple=10 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       ) &&
+       git clone . merge-10-expire &&
+       (
+               cd merge-10-expire &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 15 &&
+               git commit-graph write --reachable --split --size-multiple=10 --expire-time=1980-01-01 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 3 graph-files
+       ) &&
+       git clone --no-hardlinks . max-commits &&
+       (
+               cd max-commits &&
+               git config core.commitGraph true &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               test_commit 16 &&
+               test_commit 17 &&
+               git commit-graph write --reachable --split --max-commits=1 &&
+               test_line_count = 1 $graphdir/commit-graph-chain &&
+               ls $graphdir/graph-*.graph >graph-files &&
+               test_line_count = 1 graph-files
+       )
+'
+
+test_expect_success 'remove commit-graph-chain file after flattening' '
+       git clone . flatten &&
+       (
+               cd flatten &&
+               test_line_count = 2 $graphdir/commit-graph-chain &&
+               git commit-graph write --reachable &&
+               test_path_is_missing $graphdir/commit-graph-chain &&
+               ls $graphdir >graph-files &&
+               test_line_count = 0 graph-files
+       )
+'
+
+corrupt_file() {
+       file=$1
+       pos=$2
+       data="${3:-\0}"
+       chmod a+w "$file" &&
+       printf "$data" | dd of="$file" bs=1 seek="$pos" conv=notrunc
+}
+
+test_expect_success 'verify hashes along chain, even in shallow' '
+       git clone --no-hardlinks . verify &&
+       (
+               cd verify &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(head -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1760 "\01" &&
+               test_must_fail git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "incorrect checksum" err
+       )
+'
+
+test_expect_success 'verify --shallow does not check base contents' '
+       git clone --no-hardlinks . verify-shallow &&
+       (
+               cd verify-shallow &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(head -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1000 "\01" &&
+               git commit-graph verify --shallow &&
+               test_must_fail git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "incorrect checksum" err
+       )
+'
+
+test_expect_success 'warn on base graph chunk incorrect' '
+       git clone --no-hardlinks . base-chunk &&
+       (
+               cd base-chunk &&
+               git commit-graph verify &&
+               base_file=$graphdir/graph-$(tail -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$base_file" 1376 "\01" &&
+               git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "commit-graph chain does not match" err
+       )
+'
+
+test_expect_success 'verify after commit-graph-chain corruption' '
+       git clone --no-hardlinks . verify-chain &&
+       (
+               cd verify-chain &&
+               corrupt_file "$graphdir/commit-graph-chain" 60 "G" &&
+               git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "invalid commit-graph chain" err &&
+               corrupt_file "$graphdir/commit-graph-chain" 60 "A" &&
+               git commit-graph verify 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "unable to find all commit-graph files" err
+       )
+'
+
+test_expect_success 'verify across alternates' '
+       git clone --no-hardlinks . verify-alt &&
+       (
+               cd verify-alt &&
+               rm -rf $graphdir &&
+               altdir="$(pwd)/../.git/objects" &&
+               echo "$altdir" >.git/objects/info/alternates &&
+               git commit-graph verify --object-dir="$altdir/" &&
+               test_commit extra &&
+               git commit-graph write --reachable --split &&
+               tip_file=$graphdir/graph-$(tail -n 1 $graphdir/commit-graph-chain).graph &&
+               corrupt_file "$tip_file" 100 "\01" &&
+               test_must_fail git commit-graph verify --shallow 2>test_err &&
+               grep -v "^+" test_err >err &&
+               test_i18ngrep "commit-graph has incorrect fanout value" err
+       )
+'
+
+test_expect_success 'add octopus merge' '
+       git reset --hard commits/10 &&
+       git merge commits/3 commits/4 &&
+       git branch merge/octopus &&
+       git commit-graph write --reachable --split &&
+       git commit-graph verify &&
+       test_line_count = 3 $graphdir/commit-graph-chain
+'
+
+graph_git_behavior 'graph exists' merge/octopus commits/12
+
+test_expect_success 'split across alternate where alternate is not split' '
+       git commit-graph write --reachable &&
+       test_path_is_file .git/objects/info/commit-graph &&
+       cp .git/objects/info/commit-graph . &&
+       git clone --no-hardlinks . alt-split &&
+       (
+               cd alt-split &&
+               echo "$(pwd)"/../.git/objects >.git/objects/info/alternates &&
+               test_commit 18 &&
+               git commit-graph write --reachable --split &&
+               test_line_count = 1 $graphdir/commit-graph-chain
+       ) &&
+       test_cmp commit-graph .git/objects/info/commit-graph
+'
+
+test_done