From a13b3b630d0adf3d3b771824440023b859949db6 Mon Sep 17 00:00:00 2001 From: Victoria Dye Date: Tue, 19 Sep 2023 18:20:32 -0700 Subject: [PATCH 001/229] sparse-index.c: fix use of index hashes in expand_index In ac8acb4f2c7 (sparse-index: complete partial expansion, 2022-05-23), 'expand_index()' was updated to expand the index to a given pathspec. However, the 'path_matches_pattern_list()' method used to facilitate this has the side effect of initializing or updating the index hash variables ('name_hash', 'dir_hash', and 'name_hash_initialized'). This operation is performed on 'istate', though, not 'full'; as a result, the initialized hashes are later overwritten when copied from 'full'. To ensure the correct hashes are in 'istate' after the index expansion, change the arg used in 'path_matches_pattern_list()' from 'istate' to 'full'. Note that this does not fully solve the problem. If 'istate' does not have an initialized 'name_hash' when its contents are copied to 'full', initialized hashes will be copied back into 'istate' but 'name_hash_initialized' will be 0. Therefore, we also need to copy 'full->name_hash_initialized' back to 'istate' after the index expansion is complete. Signed-off-by: Victoria Dye --- sparse-index.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sparse-index.c b/sparse-index.c index 76f90da5f5f41e..4d75ee428660ab 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -407,7 +407,7 @@ void expand_index(struct index_state *istate, struct pattern_list *pl) if (pl && path_matches_pattern_list(ce->name, ce->ce_namelen, NULL, &dtype, - pl, istate) == NOT_MATCHED) { + pl, full) == NOT_MATCHED) { set_index_entry(full, full->cache_nr++, ce); continue; } @@ -435,6 +435,7 @@ void expand_index(struct index_state *istate, struct pattern_list *pl) } /* Copy back into original index. */ + istate->name_hash_initialized = full->name_hash_initialized; memcpy(&istate->name_hash, &full->name_hash, sizeof(full->name_hash)); memcpy(&istate->dir_hash, &full->dir_hash, sizeof(full->dir_hash)); istate->sparse_index = pl ? INDEX_PARTIALLY_SPARSE : INDEX_EXPANDED; From b924ab94622f9ec6c8c5b5b589a57b9d9b9d942b Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 25 Aug 2023 09:58:27 -0400 Subject: [PATCH 002/229] t5300: confirm failure of git index-pack when non-idx suffix requested Add test case to demonstrate that `git index-pack -o pack-path` fails if does not end in ".idx" when `--rev-index` is enabled. In e37d0b8730b (builtin/index-pack.c: write reverse indexes, 2021-01-25) we learned to create `.rev` reverse indexes in addition to `.idx` index files. The `.rev` file pathname is constructed by replacing the suffix on the `.idx` file. The code assumes a hard-coded "idx" suffix. In a8dd7e05b1c (config: enable `pack.writeReverseIndex` by default, 2023-04-12) reverse indexes were enabled by default. If the `-o ` argument is used, the index file may have a different suffix. This causes an error when it tries to create the reverse index pathname. The test here demonstrates the failure. (The test forces `--rev-index` to avoid interaction with `GIT_TEST_NO_WRITE_REV_INDEX` during CI runs.) Signed-off-by: Jeff Hostetler --- t/t5300-pack-object.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh index 73445782e74451..b2b74451364559 100755 --- a/t/t5300-pack-object.sh +++ b/t/t5300-pack-object.sh @@ -357,6 +357,31 @@ test_expect_success 'build pack index for an existing pack' ' : ' +# The `--rev-index` option of `git index-pack` is now the default, so +# a `foo.rev` REV file will be created when a `foo.idx` IDX file is +# created. Normally, these pathnames are based upon the `foo.pack` +# PACK file pathname. +# +# However, the `-o` option lets you set the pathname of the IDX file +# indepdent of the PACK file. +# +# Verify what happens if these suffixes are changed. +# +test_expect_success 'complain about index name' ' + # Normal case { .pack, .idx, .rev } + cat test-1-${packname_1}.pack >test-complain-0.pack && + git index-pack -o test-complain-0.idx --rev-index test-complain-0.pack && + test -f test-complain-0.idx && + test -f test-complain-0.rev && + + # Non .idx suffix + cat test-1-${packname_1}.pack >test-complain-1.pack && + test_must_fail git index-pack -o test-complain-1.idx-suffix --rev-index test-complain-1.pack 2>err && + grep "does not end" err && + ! test -f test-complain-1.idx-suffix && + ! test -f test-complain-1.rev +' + test_expect_success 'unpacking with --strict' ' for j in a b c d e f g From ce22fc8ad66c8940d28221e98d840afabaaca40c Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Tue, 16 Jul 2024 09:17:15 -0400 Subject: [PATCH 003/229] t: remove advice from some tests These seem to be custom tests to microsoft/git as they break without these changes, but these changes are not needed upstream. Signed-off-by: Derrick Stolee --- t/t1091-sparse-checkout-builtin.sh | 1 + t/t7002-mv-sparse-checkout.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index b2da4feaeff9ec..6db892c80041be 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -701,6 +701,7 @@ test_expect_success 'pattern-checks: contained glob characters' ' test_expect_success BSLASHPSPEC 'pattern-checks: escaped characters' ' git clone repo escaped && + git -C escaped config advice.sparseIndexExpanded false && TREEOID=$(git -C escaped rev-parse HEAD:folder1) && NEWTREE=$(git -C escaped mktree <<-EOF $(git -C escaped ls-tree HEAD) diff --git a/t/t7002-mv-sparse-checkout.sh b/t/t7002-mv-sparse-checkout.sh index 4d3f221224fb39..602c6a064b2ce6 100755 --- a/t/t7002-mv-sparse-checkout.sh +++ b/t/t7002-mv-sparse-checkout.sh @@ -155,6 +155,9 @@ test_expect_success 'mv refuses to move sparse-to-non-sparse' ' test_expect_success 'recursive mv refuses to move (possible) sparse' ' test_when_finished rm -rf b c e sub2 && + + git config advice.sparseIndexExpanded false && + git reset --hard && # Without cone mode, "sub" and "sub2" do not match git sparse-checkout set sub/dir sub2/dir && From 5e87d2da94379b43a30a38ad13fa8c8716372e52 Mon Sep 17 00:00:00 2001 From: Victoria Dye Date: Wed, 20 Sep 2023 13:12:30 -0700 Subject: [PATCH 004/229] t1092: add test for untracked files and directories Add a test verifying that sparse-checkout (with and without sparse index enabled) treat untracked files & directories correctly when changing sparse patterns. Specifically, it ensures that 'git sparse-checkout set' * deletes empty directories outside the sparse cone * does _not_ delete untracked files outside the sparse cone Signed-off-by: Victoria Dye --- t/t1092-sparse-checkout-compatibility.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh index b0f691c151a7d0..0496ee0b209fbb 100755 --- a/t/t1092-sparse-checkout-compatibility.sh +++ b/t/t1092-sparse-checkout-compatibility.sh @@ -317,6 +317,22 @@ test_expect_success 'root directory cannot be sparse' ' test_cmp expect actual ' +test_expect_success 'sparse-checkout with untracked files and dirs' ' + init_repos && + + # Empty directories outside sparse cone are deleted + run_on_sparse mkdir -p deep/empty && + test_sparse_match git sparse-checkout set folder1 && + test_must_be_empty sparse-checkout-err && + run_on_sparse test_path_is_missing deep && + + # Untracked files outside sparse cone are not deleted + run_on_sparse touch folder1/another && + test_sparse_match git sparse-checkout set folder2 && + grep "directory ${SQ}folder1/${SQ} contains untracked files" sparse-checkout-err && + run_on_sparse test_path_exists folder1/another +' + test_expect_success 'status with options' ' init_repos && test_sparse_match ls && From bc8fe5dd47f2bd2ccb16cb511e7566179a5ea70f Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 29 Apr 2024 12:47:27 -0400 Subject: [PATCH 005/229] survey: calculate more stats on refs Calculate the number of symrefs, loose vs packed, and the maximal/accumulated length of local vs remote branches. Signed-off-by: Jeff Hostetler Signed-off-by: Johannes Schindelin --- builtin/survey.c | 82 +++++++++++++++++++++++++++++++++++++++++-- t/t8100-git-survey.sh | 21 +++++++---- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index f40905fb2fd57a..ad7a6d13e154bf 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -15,9 +15,7 @@ #include "revision.h" #include "strbuf.h" #include "strvec.h" -#include "tag.h" #include "trace2.h" -#include "color.h" static const char * const survey_usage[] = { N_("(EXPERIMENTAL!) git survey "), @@ -53,6 +51,22 @@ struct survey_report_ref_summary { size_t tags_annotated_nr; size_t others_nr; size_t unknown_nr; + + size_t cnt_symref; + + size_t cnt_packed; + size_t cnt_loose; + + /* + * Measure the length of the refnames. We can look for + * potential platform limits. The partial sums may help us + * estimate the size of a haves/wants conversation, since each + * refname and a SHA must be transmitted. + */ + size_t len_max_local_refname; + size_t len_sum_local_refnames; + size_t len_max_remote_refname; + size_t len_sum_remote_refnames; }; struct survey_report_object_summary { @@ -380,6 +394,42 @@ static void survey_report_plaintext_refs(struct survey_context *ctx) free(fmt); } + /* + * SymRefs are somewhat orthogonal to the above classification (e.g. + * "HEAD" --> detached and "refs/remotes/origin/HEAD" --> remote) so the + * above classified counts will already include them, but it is less + * confusing to display them here than to create a whole new section. + */ + if (ctx->report.refs.cnt_symref) { + char *fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->cnt_symref); + insert_table_rowv(&table, _("Symbolic refs"), fmt, NULL); + free(fmt); + } + + if (ctx->report.refs.cnt_loose || ctx->report.refs.cnt_packed) { + char *fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->cnt_loose); + insert_table_rowv(&table, _("Loose refs"), fmt, NULL); + free(fmt); + fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->cnt_packed); + insert_table_rowv(&table, _("Packed refs"), fmt, NULL); + free(fmt); + } + + if (ctx->report.refs.len_max_local_refname || ctx->report.refs.len_max_remote_refname) { + char *fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->len_max_local_refname); + insert_table_rowv(&table, _("Max local refname length"), fmt, NULL); + free(fmt); + fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->len_sum_local_refnames); + insert_table_rowv(&table, _("Sum local refnames length"), fmt, NULL); + free(fmt); + fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->len_max_remote_refname); + insert_table_rowv(&table, _("Max remote refname length"), fmt, NULL); + free(fmt); + fmt = xstrfmt("%"PRIuMAX"", (uintmax_t)refs->len_sum_remote_refnames); + insert_table_rowv(&table, _("Sum remote refnames length"), fmt, NULL); + free(fmt); + } + print_table_plaintext(&table); clear_table(&table); } @@ -638,6 +688,7 @@ static void survey_phase_refs(struct survey_context *ctx) for (int i = 0; i < ctx->ref_array.nr; i++) { unsigned long size; struct ref_array_item *item = ctx->ref_array.items[i]; + size_t len = strlen(item->refname); switch (item->kind) { case FILTER_REFS_TAGS: @@ -664,6 +715,33 @@ static void survey_phase_refs(struct survey_context *ctx) ctx->report.refs.unknown_nr++; break; } + + /* + * SymRefs are somewhat orthogonal to the above + * classification (e.g. "HEAD" --> detached + * and "refs/remotes/origin/HEAD" --> remote) so + * our totals will already include them. + */ + if (item->flag & REF_ISSYMREF) + ctx->report.refs.cnt_symref++; + + /* + * Where/how is the ref stored in GITDIR. + */ + if (item->flag & REF_ISPACKED) + ctx->report.refs.cnt_packed++; + else + ctx->report.refs.cnt_loose++; + + if (item->kind == FILTER_REFS_REMOTES) { + ctx->report.refs.len_sum_remote_refnames += len; + if (len > ctx->report.refs.len_max_remote_refname) + ctx->report.refs.len_max_remote_refname = len; + } else { + ctx->report.refs.len_sum_local_refnames += len; + if (len > ctx->report.refs.len_max_local_refname) + ctx->report.refs.len_max_local_refname = len; + } } trace2_region_leave("survey", "phase/refs", ctx->repo); diff --git a/t/t8100-git-survey.sh b/t/t8100-git-survey.sh index 1ba48cc47e1b35..ca88bf352f249d 100755 --- a/t/t8100-git-survey.sh +++ b/t/t8100-git-survey.sh @@ -64,13 +64,20 @@ test_expect_success 'git survey (default)' ' ----------------------------------------------------- REFERENCES SUMMARY - ======================== - , Ref Type | Count - -----------------+------ - , Branches | 1 - Remote refs | 0 - Tags (all) | 2 - Tags (annotated) | 2 + ================================== + , Ref Type | Count + ---------------------------+------ + , Branches | 1 + , Remote refs | 0 + , Tags (all) | 2 + , Tags (annotated) | 2 + , Symbolic refs | 1 + , Loose refs | 4 + , Packed refs | 0 + Max local refname length | 15 + Sum local refnames length | 46 + Max remote refname length | 0 + Sum remote refnames length | 0 REACHABLE OBJECT SUMMARY ======================== From 29cdb90a161d0de02ab193063cd07d9bc526fabd Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 25 Aug 2023 11:06:28 -0400 Subject: [PATCH 006/229] index-pack: disable rev-index if index file has non .idx suffix Teach index-pack to silently omit the reverse index if the index file does not have the standard ".idx" suffix. In e37d0b8730b (builtin/index-pack.c: write reverse indexes, 2021-01-25) we learned to create `.rev` reverse indexes in addition to `.idx` index files. The `.rev` file pathname is constructed by replacing the suffix on the `.idx` file. The code assumes a hard-coded "idx" suffix. In a8dd7e05b1c (config: enable `pack.writeReverseIndex` by default, 2023-04-12) reverse indexes were enabled by default. If the `-o ` argument is used, the index file may have a different suffix. This causes an error when it tries to create the reverse index pathname. Since we do not know why the user requested a non-standard suffix for the index, we cannot guess what the proper corresponding suffix should be for the reverse index. So we disable it. The t5300 test has been updated to verify that we no longer error out and that the .rev file is not created. TODO We could warn the user that we skipped it (perhaps only if they TODO explicitly requested `--rev-index` on the command line). TODO TODO Ideally, we should add an `--rev-index-path=` argument TODO or change `--rev-index` to take a pathname. TODO TODO I'll leave these questions for a future series. Signed-off-by: Jeff Hostetler --- builtin/index-pack.c | 4 ++++ t/t5300-pack-object.sh | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index 2b78ba7fe4d14a..b56ebaf74bc331 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -1897,6 +1897,7 @@ int cmd_index_pack(int argc, unsigned foreign_nr = 1; /* zero is a "good" value, assume bad */ int report_end_of_input = 0; int hash_algo = 0; + int dash_o = 0; /* * index-pack never needs to fetch missing objects except when @@ -1981,6 +1982,7 @@ int cmd_index_pack(int argc, if (index_name || (i+1) >= argc) usage(index_pack_usage); index_name = argv[++i]; + dash_o = 1; } else if (starts_with(arg, "--index-version=")) { char *c; opts.version = strtoul(arg + 16, &c, 10); @@ -2034,6 +2036,8 @@ int cmd_index_pack(int argc, repo_set_hash_algo(the_repository, GIT_HASH_DEFAULT); opts.flags &= ~(WRITE_REV | WRITE_REV_VERIFY); + if (rev_index && dash_o && !ends_with(index_name, ".idx")) + rev_index = 0; if (rev_index) { opts.flags |= verify ? WRITE_REV_VERIFY : WRITE_REV; if (index_name) diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh index b2b74451364559..dc375529da7466 100755 --- a/t/t5300-pack-object.sh +++ b/t/t5300-pack-object.sh @@ -374,11 +374,10 @@ test_expect_success 'complain about index name' ' test -f test-complain-0.idx && test -f test-complain-0.rev && - # Non .idx suffix + # Non .idx suffix -- implicitly omits the .rev cat test-1-${packname_1}.pack >test-complain-1.pack && - test_must_fail git index-pack -o test-complain-1.idx-suffix --rev-index test-complain-1.pack 2>err && - grep "does not end" err && - ! test -f test-complain-1.idx-suffix && + git index-pack -o test-complain-1.idx-suffix --rev-index test-complain-1.pack && + test -f test-complain-1.idx-suffix && ! test -f test-complain-1.rev ' From 06a97228726b380e5360963e22c883fe8d83b3e2 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 26 Jun 2024 12:27:41 -0400 Subject: [PATCH 007/229] trace2: prefetch value of GIT_TRACE2_DST_DEBUG at startup Prefetch the value of GIT_TRACE2_DST_DEBUG during startup and before we try to open any Trace2 destination pathnames. Normally, Trace2 always silently fails if a destination target cannot be opened so that it doesn't affect the execution of a Git command. The command should run normally, but just not generate any trace data. This can make it difficult to debug a telemetry setup, since the user doesn't know why telemetry isn't being generated. If the environment variable GIT_TRACE2_DST_DEBUG is true, the Trace2 startup will print a warning message with the `errno` to make debugging easier. However, on Windows, looking up the env variable resets `errno` so the warning message always ends with `...tracing: No error` which is not very helpful. Prefetch the env variable at startup. This avoids the need to update each call-site to capture `errno` in the usual `saved-errno` variable. Signed-off-by: Jeff Hostetler --- trace2.c | 10 ++++++++++ trace2/tr2_dst.c | 2 +- trace2/tr2_dst.h | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/trace2.c b/trace2.c index c23c0a227b7032..24dd7b1be9fbbe 100644 --- a/trace2.c +++ b/trace2.c @@ -227,6 +227,16 @@ void trace2_initialize_fl(const char *file, int line) if (!tr2_tgt_want_builtins()) return; trace2_enabled = 1; + + /* + * getenv() on Windows stomps on `errno` and the code in + * tr2_dst.c verifies that warnings are enabled before + * formatting the warning message (and calling strerror()). + * So prefetch the value from the environment before we need + * it. + */ + tr2_dst_want_warning(); + if (!git_env_bool("GIT_TRACE2_REDACT", 1)) trace2_redact = 0; diff --git a/trace2/tr2_dst.c b/trace2/tr2_dst.c index 5be892cd5cdefa..61579f24bdbde3 100644 --- a/trace2/tr2_dst.c +++ b/trace2/tr2_dst.c @@ -24,7 +24,7 @@ */ static int tr2env_max_files = 0; -static int tr2_dst_want_warning(void) +int tr2_dst_want_warning(void) { static int tr2env_dst_debug = -1; diff --git a/trace2/tr2_dst.h b/trace2/tr2_dst.h index b1a8c144e073ba..4166539eb9e100 100644 --- a/trace2/tr2_dst.h +++ b/trace2/tr2_dst.h @@ -35,4 +35,16 @@ int tr2_dst_trace_want(struct tr2_dst *dst); */ void tr2_dst_write_line(struct tr2_dst *dst, struct strbuf *buf_line); +/* + * Return true if we want warning messages when trying to open a + * destination. + * + * (Trace2 always silently fails if a target cannot be opened so that + * we don't affect the execution of the Git command, but it is helpful + * for debugging telemetry configuration if we log warning messages + * when trying to open a target. This is controlled by another config + * value.) + */ +int tr2_dst_want_warning(void); + #endif /* TR2_DST_H */ From cc9d08f9b281e9142e78c4de60ef893c2cb118b9 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 29 Apr 2024 15:40:00 -0400 Subject: [PATCH 008/229] survey: show some commits/trees/blobs histograms With this commit, we gather statistics about the sizes of commits, trees, and blobs in the repository, and then present them in the form of "hexbins", i.e. log(16) histograms that show how many objects fall into the 0..15 bytes range, the 16..255 range, the 256..4095 range, etc. For commits, we also show the total count grouped by the number of parents, and for trees we additionally show the total count grouped by number of entries in the form of "qbins", i.e. log(4) histograms. Signed-off-by: Jeff Hostetler Signed-off-by: Johannes Schindelin --- builtin/survey.c | 338 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 334 insertions(+), 4 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index ad7a6d13e154bf..cd3f17aa467a24 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -16,6 +16,8 @@ #include "strbuf.h" #include "strvec.h" #include "trace2.h" +#include "tree.h" +#include "tree-walk.h" static const char * const survey_usage[] = { N_("(EXPERIMENTAL!) git survey "), @@ -69,11 +71,162 @@ struct survey_report_ref_summary { size_t len_sum_remote_refnames; }; +/* + * HBIN -- hex binning (histogram bucketing). + * + * We create histograms for various counts and sums. Since we have a + * wide range of values (objects range in size from 1 to 4G bytes), a + * linear bucketing is not interesting. Instead, lets use a + * log16()-based bucketing. This gives us a better spread on the low + * and middle range and a coarse bucketing on the high end. + * + * The idea here is that it doesn't matter if you have n 1GB blobs or + * n/2 1GB blobs and n/2 1.5GB blobs -- either way you have a scaling + * problem that we want to report on. + */ +#define HBIN_LEN (sizeof(unsigned long) * 2) +#define HBIN_MASK (0xF) +#define HBIN_SHIFT (4) + +static int hbin(unsigned long value) +{ + for (size_t k = 0; k < HBIN_LEN; k++) { + if ((value & ~(HBIN_MASK)) == 0) + return k; + value >>= HBIN_SHIFT; + } + + return 0; /* should not happen */ +} + +/* + * QBIN -- base4 binning (histogram bucketing). + * + * This is the same idea as the above, but we want better granularity + * in the low end and don't expect as many large values. + */ +#define QBIN_LEN (sizeof(unsigned long) * 4) +#define QBIN_MASK (0x3) +#define QBIN_SHIFT (2) + +static int qbin(unsigned long value) +{ + for (size_t k = 0; k < QBIN_LEN; k++) { + if ((value & ~(QBIN_MASK)) == 0) + return k; + value >>= (QBIN_SHIFT); + } + + return 0; /* should not happen */ +} + +/* + * histogram bin for objects. + */ +struct obj_hist_bin { + uint64_t sum_size; /* sum(object_size) for all objects in this bin */ + uint64_t sum_disk_size; /* sum(on_disk_size) for all objects in this bin */ + uint32_t cnt_seen; /* number seen in this bin */ +}; + +static void incr_obj_hist_bin(struct obj_hist_bin *pbin, + unsigned long object_length, + off_t disk_sizep) +{ + pbin->sum_size += object_length; + pbin->sum_disk_size += disk_sizep; + pbin->cnt_seen++; +} + +/* + * Common fields for any type of object. + */ +struct survey_stats_base_object { + uint32_t cnt_seen; + + uint32_t cnt_missing; /* we may have a partial clone. */ + + /* + * Number of objects grouped by where they are stored on disk. + * This is a function of how the ODB is packed. + */ + uint32_t cnt_cached; /* see oi.whence */ + uint32_t cnt_loose; /* see oi.whence */ + uint32_t cnt_packed; /* see oi.whence */ + uint32_t cnt_dbcached; /* see oi.whence */ + + uint64_t sum_size; /* sum(object_size) */ + uint64_t sum_disk_size; /* sum(disk_size) */ + + /* + * A histogram of the count of objects, the observed size, and + * the on-disk size grouped by the observed size. + */ + struct obj_hist_bin size_hbin[HBIN_LEN]; +}; + +/* + * PBIN -- parent vector binning (histogram bucketing). + * + * We create a histogram based upon the number of parents + * in a commit. This is a simple linear vector. It starts + * at zero for "initial" commits. + * + * If a commit has more parents, just put it in the last bin. + */ +#define PBIN_VEC_LEN (32) + +struct survey_stats_commits { + struct survey_stats_base_object base; + + /* + * Count of commits with k parents. + */ + uint32_t parent_cnt_pbin[PBIN_VEC_LEN]; +}; + +/* + * Stats for reachable trees. + */ +struct survey_stats_trees { + struct survey_stats_base_object base; + + /* + * In the following, nr_entries refers to the number of files or + * subdirectories in a tree. We are interested in how wide the + * tree is and if the repo has gigantic directories. + */ + uint64_t max_entries; /* max(nr_entries) -- the width of the largest tree */ + + /* + * Computing the sum of the number of entries across all trees + * is probably not that interesting. + */ + uint64_t sum_entries; /* sum(nr_entries) -- sum across all trees */ + + /* + * A histogram of the count of trees, the observed size, and + * the on-disk size grouped by the number of entries in the tree. + */ + struct obj_hist_bin entry_qbin[QBIN_LEN]; +}; + +/* + * Stats for reachable blobs. + */ +struct survey_stats_blobs { + struct survey_stats_base_object base; +}; + struct survey_report_object_summary { size_t commits_nr; size_t tags_nr; size_t trees_nr; size_t blobs_nr; + + struct survey_stats_commits commits; + struct survey_stats_trees trees; + struct survey_stats_blobs blobs; }; /** @@ -363,6 +516,98 @@ static void print_table_plaintext(struct survey_table *table) free(column_widths); } +static void pretty_print_bin_table(const char *title_caption, + const char *bucket_header, + struct obj_hist_bin *bin, + uint64_t bin_len, int bin_shift, uint64_t bin_mask) +{ + struct survey_table table = SURVEY_TABLE_INIT; + struct strbuf bucket = STRBUF_INIT, cnt_seen = STRBUF_INIT; + struct strbuf sum_size = STRBUF_INIT, sum_disk_size = STRBUF_INIT; + uint64_t lower = 0; + uint64_t upper = bin_mask; + + table.table_name = title_caption; + strvec_pushl(&table.header, bucket_header, "Count", "Size", "Disk Size", NULL); + + for (size_t k = 0; k < bin_len; k++) { + struct obj_hist_bin *p = bin + k; + uintmax_t lower_k = lower; + uintmax_t upper_k = upper; + + lower = upper+1; + upper = (upper << bin_shift) + bin_mask; + + if (!p->cnt_seen) + continue; + + strbuf_reset(&bucket); + strbuf_addf(&bucket, "%"PRIuMAX"..%"PRIuMAX, lower_k, upper_k); + + strbuf_reset(&cnt_seen); + strbuf_addf(&cnt_seen, "%"PRIuMAX, (uintmax_t)p->cnt_seen); + + strbuf_reset(&sum_size); + strbuf_addf(&sum_size, "%"PRIuMAX, (uintmax_t)p->sum_size); + + strbuf_reset(&sum_disk_size); + strbuf_addf(&sum_disk_size, "%"PRIuMAX, (uintmax_t)p->sum_disk_size); + + insert_table_rowv(&table, bucket.buf, + cnt_seen.buf, sum_size.buf, sum_disk_size.buf, NULL); + } + strbuf_release(&bucket); + strbuf_release(&cnt_seen); + strbuf_release(&sum_size); + strbuf_release(&sum_disk_size); + + print_table_plaintext(&table); + clear_table(&table); +} + +static void survey_report_hbin(const char *title_caption, + struct obj_hist_bin *bin) +{ + pretty_print_bin_table(title_caption, + "Byte Range", + bin, + HBIN_LEN, HBIN_SHIFT, HBIN_MASK); +} + +static void survey_report_tree_lengths(struct survey_context *ctx) +{ + pretty_print_bin_table(_("TREE HISTOGRAM BY NUMBER OF ENTRIES"), + "Entry Range", + ctx->report.reachable_objects.trees.entry_qbin, + QBIN_LEN, QBIN_SHIFT, QBIN_MASK); +} + +static void survey_report_commit_parents(struct survey_context *ctx) +{ + struct survey_stats_commits *psc = &ctx->report.reachable_objects.commits; + struct survey_table table = SURVEY_TABLE_INIT; + struct strbuf parents = STRBUF_INIT, counts = STRBUF_INIT; + + table.table_name = _("HISTOGRAM BY NUMBER OF COMMIT PARENTS"); + strvec_pushl(&table.header, "Parents", "Counts", NULL); + + for (int k = 0; k < PBIN_VEC_LEN; k++) + if (psc->parent_cnt_pbin[k]) { + strbuf_reset(&parents); + strbuf_addf(&parents, "%02d", k); + + strbuf_reset(&counts); + strbuf_addf(&counts, "%14"PRIuMAX, (uintmax_t)psc->parent_cnt_pbin[k]); + + insert_table_rowv(&table, parents.buf, counts.buf, NULL); + } + strbuf_release(&parents); + strbuf_release(&counts); + + print_table_plaintext(&table); + clear_table(&table); +} + static void survey_report_plaintext_refs(struct survey_context *ctx) { struct survey_report_ref_summary *refs = &ctx->report.refs; @@ -515,6 +760,19 @@ static void survey_report_plaintext(struct survey_context *ctx) ctx->report.by_type, REPORT_TYPE_COUNT); + survey_report_commit_parents(ctx); + + survey_report_hbin(_("COMMITS HISTOGRAM BY SIZE IN BYTES"), + ctx->report.reachable_objects.commits.base.size_hbin); + + survey_report_tree_lengths(ctx); + + survey_report_hbin(_("TREES HISTOGRAM BY SIZE IN BYTES"), + ctx->report.reachable_objects.trees.base.size_hbin); + + survey_report_hbin(_("BLOBS HISTOGRAM BY SIZE IN BYTES"), + ctx->report.reachable_objects.blobs.base.size_hbin); + survey_report_plaintext_sorted_size( &ctx->report.top_paths_by_count[REPORT_TYPE_TREE]); survey_report_plaintext_sorted_size( @@ -784,6 +1042,8 @@ static void increment_totals(struct survey_context *ctx, unsigned long object_length = 0; off_t disk_sizep = 0; enum object_type type; + struct survey_stats_base_object *base; + int hb; oi.typep = &type; oi.sizep = &object_length; @@ -793,11 +1053,81 @@ static void increment_totals(struct survey_context *ctx, &oids->oid[i], &oi, oi_flags) < 0) { summary->num_missing++; - } else { - summary->nr++; - summary->disk_size += disk_sizep; - summary->inflated_size += object_length; + continue; + } + + summary->nr++; + summary->disk_size += disk_sizep; + summary->inflated_size += object_length; + + switch (type) { + case OBJ_COMMIT: { + struct commit *commit = lookup_commit(ctx->repo, &oids->oid[i]); + unsigned k = commit_list_count(commit->parents); + + if (k >= PBIN_VEC_LEN) + k = PBIN_VEC_LEN - 1; + + ctx->report.reachable_objects.commits.parent_cnt_pbin[k]++; + base = &ctx->report.reachable_objects.commits.base; + break; } + case OBJ_TREE: { + struct tree *tree = lookup_tree(ctx->repo, &oids->oid[i]); + if (tree) { + struct survey_stats_trees *pst = &ctx->report.reachable_objects.trees; + struct tree_desc desc; + struct name_entry entry; + uint64_t nr_entries; + int qb; + + parse_tree(tree); + init_tree_desc(&desc, &oids->oid[i], tree->buffer, tree->size); + nr_entries = 0; + while (tree_entry(&desc, &entry)) + nr_entries++; + + pst->sum_entries += nr_entries; + + if (nr_entries > pst->max_entries) + pst->max_entries = nr_entries; + + qb = qbin(nr_entries); + incr_obj_hist_bin(&pst->entry_qbin[qb], object_length, disk_sizep); + } + base = &ctx->report.reachable_objects.trees.base; + break; + } + case OBJ_BLOB: + base = &ctx->report.reachable_objects.blobs.base; + break; + default: + continue; + } + + switch (oi.whence) { + case OI_CACHED: + base->cnt_cached++; + break; + case OI_LOOSE: + base->cnt_loose++; + break; + case OI_PACKED: + base->cnt_packed++; + break; + case OI_DBCACHED: + base->cnt_dbcached++; + break; + default: + break; + } + + base->sum_size += object_length; + base->sum_disk_size += disk_sizep; + + hb = hbin(object_length); + incr_obj_hist_bin(&base->size_hbin[hb], object_length, disk_sizep); + } } From e7f1e6a8cd1796fa73a350691c7443889913b8f0 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 1 May 2024 12:56:38 -0400 Subject: [PATCH 009/229] survey: add vector of largest objects for various scaling dimensions Create `struct large_item` and `struct large_item_vec` to capture the n largest commits, trees, and blobs under various scaling dimensions, such as size in bytes, number of commit parents, or number of entries in a tree. Each of these have a command line option to set them independently. Signed-off-by: Jeff Hostetler --- Documentation/config/survey.adoc | 29 ++++ Documentation/git-survey.adoc | 31 +++++ builtin/survey.c | 222 ++++++++++++++++++++++++++++++- 3 files changed, 276 insertions(+), 6 deletions(-) diff --git a/Documentation/config/survey.adoc b/Documentation/config/survey.adoc index 9e594a2092f225..fd2d7e153b38ce 100644 --- a/Documentation/config/survey.adoc +++ b/Documentation/config/survey.adoc @@ -11,4 +11,33 @@ survey.*:: top:: This integer value implies `--top=`, specifying the number of entries in the detail tables. + showBlobSizes:: + A non-negative integer value. Requests details on the + largest file blobs by size in bytes. Provides a + default value for `--blob-sizes=` in + linkgit:git-survey[1]. + showCommitParents:: + A non-negative integer value. Requests details on the + commits with the most number of parents. Provides a + default value for `--commit-parents=` in + linkgit:git-survey[1]. + showCommitSizes:: + A non-negative integer value. Requests details on the + largest commits by size in bytes. Generally, these + are the commits with the largest commit messages. + Provides a default value for `--commit-sizes=` in + linkgit:git-survey[1]. + showTreeEntries:: + A non-negative integer value. Requests details on the + trees (directories) with the most number of entries + (files and subdirectories). Provides a default value + for `--tree-entries=` in linkgit:git-survey[1]. + showTreeSizes:: + A non-negative integer value. Requests details on the + largest trees (directories) by size in bytes. This + will set will usually be equal to the + `survey.showTreeEntries` set, but may be skewed by very + long file or subdirectory entry names. Provides a + default value for `--tree-sizes=` in + linkgit:git-survey[1]. -- diff --git a/Documentation/git-survey.adoc b/Documentation/git-survey.adoc index 44f3a0568b7697..f73f1706b6526f 100644 --- a/Documentation/git-survey.adoc +++ b/Documentation/git-survey.adoc @@ -59,6 +59,32 @@ only refs for the given options are added. --other:: Add notes (`refs/notes/`) and stashes (`refs/stash/`) to the set. +Large Item Selection +~~~~~~~~~~~~~~~~~~~~ + +The following options control the optional display of large items under +various dimensions of scale. The OID of the largest `n` objects will be +displayed in reverse sorted order. For each, `n` defaults to 10. + +--commit-parents:: + Shows the OIDs of the commits with the most parent commits. + +--commit-sizes:: + Shows the OIDs of the largest commits by size in bytes. This is + usually the ones with the largest commit messages. + +--tree-entries:: + Shows the OIDs of the trees with the most number of entries. These + are the directories with the most number of files or subdirectories. + +--tree-sizes:: + Shows the OIDs of the largest trees by size in bytes. This set + will usually be the same as the vector of number of entries unless + skewed by very long entry names. + +--blob-sizes:: + Shows the OIDs of the largest blobs by size in bytes. + OUTPUT ------ @@ -78,6 +104,11 @@ Reachable Object Summary The reachable object summary shows the total number of each kind of Git object, including tags, commits, trees, and blobs. +CONFIGURATION +------------- + +include::config/survey.adoc[] + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/survey.c b/builtin/survey.c index cd3f17aa467a24..3ef5dc7a5a3806 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -41,6 +41,15 @@ static struct survey_refs_wanted default_ref_options = { struct survey_opts { int verbose; int show_progress; + + int show_largest_commits_by_nr_parents; + int show_largest_commits_by_size_bytes; + + int show_largest_trees_by_nr_entries; + int show_largest_trees_by_size_bytes; + + int show_largest_blobs_by_size_bytes; + int top_nr; struct survey_refs_wanted refs; }; @@ -138,6 +147,87 @@ static void incr_obj_hist_bin(struct obj_hist_bin *pbin, pbin->cnt_seen++; } +/* + * Remember the largest n objects for some scaling dimension. This + * could be the observed object size or number of entries in a tree. + * We'll use this to generate a sorted vector in the output for that + * dimension. + */ +struct large_item { + uint64_t size; + struct object_id oid; +}; + +struct large_item_vec { + char *dimension_label; + char *item_label; + uint64_t nr_items; + struct large_item items[FLEX_ARRAY]; /* nr_items */ +}; + +static struct large_item_vec *alloc_large_item_vec(const char *dimension_label, + const char *item_label, + uint64_t nr_items) +{ + struct large_item_vec *vec; + size_t flex_len = nr_items * sizeof(struct large_item); + + if (!nr_items) + return NULL; + + vec = xcalloc(1, (sizeof(struct large_item_vec) + flex_len)); + vec->dimension_label = strdup(dimension_label); + vec->item_label = strdup(item_label); + vec->nr_items = nr_items; + + return vec; +} + +static void free_large_item_vec(struct large_item_vec *vec) +{ + if (!vec) + return; + + free(vec->dimension_label); + free(vec->item_label); + free(vec); +} + +static void maybe_insert_large_item(struct large_item_vec *vec, + uint64_t size, + struct object_id *oid) +{ + size_t rest_len; + size_t k; + + if (!vec || !vec->nr_items) + return; + + /* + * Since the odds an object being among the largest n + * is small, shortcut and see if it is smaller than + * the smallest one in our set and quickly reject it. + */ + if (size < vec->items[vec->nr_items - 1].size) + return; + + for (k = 0; k < vec->nr_items; k++) { + if (size < vec->items[k].size) + continue; + + /* push items[k..] down one and insert it here */ + + rest_len = (vec->nr_items - k - 1) * sizeof(struct large_item); + if (rest_len) + memmove(&vec->items[k + 1], &vec->items[k], rest_len); + + memset(&vec->items[k], 0, sizeof(struct large_item)); + vec->items[k].size = size; + oidcpy(&vec->items[k].oid, oid); + return; + } +} + /* * Common fields for any type of object. */ @@ -183,6 +273,9 @@ struct survey_stats_commits { * Count of commits with k parents. */ uint32_t parent_cnt_pbin[PBIN_VEC_LEN]; + + struct large_item_vec *vec_largest_by_nr_parents; + struct large_item_vec *vec_largest_by_size_bytes; }; /* @@ -192,11 +285,18 @@ struct survey_stats_trees { struct survey_stats_base_object base; /* - * In the following, nr_entries refers to the number of files or - * subdirectories in a tree. We are interested in how wide the - * tree is and if the repo has gigantic directories. + * Keep a vector of the trees with the most number of entries. + * This gives us a feel for the width of a tree when there are + * gigantic directories. */ - uint64_t max_entries; /* max(nr_entries) -- the width of the largest tree */ + struct large_item_vec *vec_largest_by_nr_entries; + + /* + * Keep a vector of the trees with the largest size in bytes. + * The contents of this may or may not match items in the other + * vector, since entryname length can alter the results. + */ + struct large_item_vec *vec_largest_by_size_bytes; /* * Computing the sum of the number of entries across all trees @@ -216,6 +316,11 @@ struct survey_stats_trees { */ struct survey_stats_blobs { struct survey_stats_base_object base; + + /* + * Remember the OIDs of the largest n blobs. + */ + struct large_item_vec *vec_largest_by_size_bytes; }; struct survey_report_object_summary { @@ -396,6 +501,12 @@ struct survey_context { static void clear_survey_context(struct survey_context *ctx) { + free_large_item_vec(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents); + free_large_item_vec(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes); + free_large_item_vec(ctx->report.reachable_objects.trees.vec_largest_by_nr_entries); + free_large_item_vec(ctx->report.reachable_objects.trees.vec_largest_by_size_bytes); + free_large_item_vec(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes); + ref_array_clear(&ctx->ref_array); strvec_clear(&ctx->refs); } @@ -608,6 +719,32 @@ static void survey_report_commit_parents(struct survey_context *ctx) clear_table(&table); } +static void survey_report_largest_vec(struct large_item_vec *vec) +{ + struct survey_table table = SURVEY_TABLE_INIT; + struct strbuf size = STRBUF_INIT; + + if (!vec || !vec->nr_items) + return; + + table.table_name = vec->dimension_label; + strvec_pushl(&table.header, "Size", "OID", NULL); + + for (size_t k = 0; k < vec->nr_items; k++) { + struct large_item *pk = &vec->items[k]; + if (!is_null_oid(&pk->oid)) { + strbuf_reset(&size); + strbuf_addf(&size, "%"PRIuMAX, (uintmax_t)pk->size); + + insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), NULL); + } + } + strbuf_release(&size); + + print_table_plaintext(&table); + clear_table(&table); +} + static void survey_report_plaintext_refs(struct survey_context *ctx) { struct survey_report_ref_summary *refs = &ctx->report.refs; @@ -787,6 +924,12 @@ static void survey_report_plaintext(struct survey_context *ctx) &ctx->report.top_paths_by_inflate[REPORT_TYPE_TREE]); survey_report_plaintext_sorted_size( &ctx->report.top_paths_by_inflate[REPORT_TYPE_BLOB]); + + survey_report_largest_vec(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents); + survey_report_largest_vec(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes); + survey_report_largest_vec(ctx->report.reachable_objects.trees.vec_largest_by_nr_entries); + survey_report_largest_vec(ctx->report.reachable_objects.trees.vec_largest_by_size_bytes); + survey_report_largest_vec(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes); } /* @@ -858,6 +1001,27 @@ static int survey_load_config_cb(const char *var, const char *value, ctx->opts.show_progress = git_config_bool(var, value); return 0; } + if (!strcmp(var, "survey.showcommitparents")) { + ctx->opts.show_largest_commits_by_nr_parents = git_config_ulong(var, value, cctx->kvi); + return 0; + } + if (!strcmp(var, "survey.showcommitsizes")) { + ctx->opts.show_largest_commits_by_size_bytes = git_config_ulong(var, value, cctx->kvi); + return 0; + } + + if (!strcmp(var, "survey.showtreeentries")) { + ctx->opts.show_largest_trees_by_nr_entries = git_config_ulong(var, value, cctx->kvi); + return 0; + } + if (!strcmp(var, "survey.showtreesizes")) { + ctx->opts.show_largest_trees_by_size_bytes = git_config_ulong(var, value, cctx->kvi); + return 0; + } + if (!strcmp(var, "survey.showblobsizes")) { + ctx->opts.show_largest_blobs_by_size_bytes = git_config_ulong(var, value, cctx->kvi); + return 0; + } if (!strcmp(var, "survey.top")) { ctx->opts.top_nr = git_config_bool(var, value); return 0; @@ -1070,6 +1234,9 @@ static void increment_totals(struct survey_context *ctx, ctx->report.reachable_objects.commits.parent_cnt_pbin[k]++; base = &ctx->report.reachable_objects.commits.base; + + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents, k, &commit->object.oid); + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes, object_length, &commit->object.oid); break; } case OBJ_TREE: { @@ -1089,8 +1256,8 @@ static void increment_totals(struct survey_context *ctx, pst->sum_entries += nr_entries; - if (nr_entries > pst->max_entries) - pst->max_entries = nr_entries; + maybe_insert_large_item(pst->vec_largest_by_nr_entries, nr_entries, &tree->object.oid); + maybe_insert_large_item(pst->vec_largest_by_size_bytes, object_length, &tree->object.oid); qb = qbin(nr_entries); incr_obj_hist_bin(&pst->entry_qbin[qb], object_length, disk_sizep); @@ -1100,6 +1267,8 @@ static void increment_totals(struct survey_context *ctx, } case OBJ_BLOB: base = &ctx->report.reachable_objects.blobs.base; + + maybe_insert_large_item(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes, object_length, &oids->oid[i]); break; default: continue; @@ -1308,6 +1477,14 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor OPT_BOOL_F(0, "detached", &ctx.opts.refs.want_detached, N_("include detached HEAD"), PARSE_OPT_NONEG), OPT_BOOL_F(0, "other", &ctx.opts.refs.want_other, N_("include notes and stashes"), PARSE_OPT_NONEG), + OPT_INTEGER_F(0, "commit-parents", &ctx.opts.show_largest_commits_by_nr_parents, N_("show N largest commits by parent count"), PARSE_OPT_NONEG), + OPT_INTEGER_F(0, "commit-sizes", &ctx.opts.show_largest_commits_by_size_bytes, N_("show N largest commits by size in bytes"), PARSE_OPT_NONEG), + + OPT_INTEGER_F(0, "tree-entries", &ctx.opts.show_largest_trees_by_nr_entries, N_("show N largest trees by entry count"), PARSE_OPT_NONEG), + OPT_INTEGER_F(0, "tree-sizes", &ctx.opts.show_largest_trees_by_size_bytes, N_("show N largest trees by size in bytes"), PARSE_OPT_NONEG), + + OPT_INTEGER_F(0, "blob-sizes", &ctx.opts.show_largest_blobs_by_size_bytes, N_("show N largest blobs by size in bytes"), PARSE_OPT_NONEG), + OPT_END(), }; @@ -1331,6 +1508,39 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor fixup_refs_wanted(&ctx); + if (ctx.opts.show_largest_commits_by_nr_parents) + ctx.report.reachable_objects.commits.vec_largest_by_nr_parents = + alloc_large_item_vec( + "largest_commits_by_nr_parents", + "nr_parents", + ctx.opts.show_largest_commits_by_nr_parents); + if (ctx.opts.show_largest_commits_by_size_bytes) + ctx.report.reachable_objects.commits.vec_largest_by_size_bytes = + alloc_large_item_vec( + "largest_commits_by_size_bytes", + "size", + ctx.opts.show_largest_commits_by_size_bytes); + + if (ctx.opts.show_largest_trees_by_nr_entries) + ctx.report.reachable_objects.trees.vec_largest_by_nr_entries = + alloc_large_item_vec( + "largest_trees_by_nr_entries", + "nr_entries", + ctx.opts.show_largest_trees_by_nr_entries); + if (ctx.opts.show_largest_trees_by_size_bytes) + ctx.report.reachable_objects.trees.vec_largest_by_size_bytes = + alloc_large_item_vec( + "largest_trees_by_size_bytes", + "size", + ctx.opts.show_largest_trees_by_size_bytes); + + if (ctx.opts.show_largest_blobs_by_size_bytes) + ctx.report.reachable_objects.blobs.vec_largest_by_size_bytes = + alloc_large_item_vec( + "largest_blobs_by_size_bytes", + "size", + ctx.opts.show_largest_blobs_by_size_bytes); + survey_phase_refs(&ctx); survey_phase_objects(&ctx); From 4db694f718dff816c0cf632e8c50393b19ddc199 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 15 May 2024 15:56:36 -0400 Subject: [PATCH 010/229] survey: add pathname of blob or tree to large_item_vec Include the pathname of each blob or tree in the large_item_vec to help identify the file or directory associated with the OID and size information. This pathname is computed during the path walk, so it reflects the first observed pathname seen for that OID during the traversal over all of the refs. Since the file or directory could have moved (without being modified), there may be multiple "correct" pathnames for a particular OID. Since we do not control the ref traversal order, we should consider it to be a "suggested pathname" for the OID. Signed-off-by: Jeff Hostetler --- builtin/survey.c | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index 3ef5dc7a5a3806..f5ff34273e604e 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -156,6 +156,7 @@ static void incr_obj_hist_bin(struct obj_hist_bin *pbin, struct large_item { uint64_t size; struct object_id oid; + struct strbuf name; }; struct large_item_vec { @@ -171,6 +172,7 @@ static struct large_item_vec *alloc_large_item_vec(const char *dimension_label, { struct large_item_vec *vec; size_t flex_len = nr_items * sizeof(struct large_item); + size_t k; if (!nr_items) return NULL; @@ -180,6 +182,9 @@ static struct large_item_vec *alloc_large_item_vec(const char *dimension_label, vec->item_label = strdup(item_label); vec->nr_items = nr_items; + for (k = 0; k < nr_items; k++) + strbuf_init(&vec->items[k].name, 0); + return vec; } @@ -188,6 +193,9 @@ static void free_large_item_vec(struct large_item_vec *vec) if (!vec) return; + for (size_t k = 0; k < vec->nr_items; k++) + strbuf_release(&vec->items[k].name); + free(vec->dimension_label); free(vec->item_label); free(vec); @@ -195,7 +203,8 @@ static void free_large_item_vec(struct large_item_vec *vec) static void maybe_insert_large_item(struct large_item_vec *vec, uint64_t size, - struct object_id *oid) + struct object_id *oid, + const char *name) { size_t rest_len; size_t k; @@ -215,7 +224,14 @@ static void maybe_insert_large_item(struct large_item_vec *vec, if (size < vec->items[k].size) continue; - /* push items[k..] down one and insert it here */ + /* + * The last large_item in the vector is about to be + * overwritten by the previous one during the shift. + * Steal its allocated strbuf and reuse it. + */ + strbuf_release(&vec->items[vec->nr_items - 1].name); + + /* push items[k..] down one and insert data for this item here */ rest_len = (vec->nr_items - k - 1) * sizeof(struct large_item); if (rest_len) @@ -224,6 +240,10 @@ static void maybe_insert_large_item(struct large_item_vec *vec, memset(&vec->items[k], 0, sizeof(struct large_item)); vec->items[k].size = size; oidcpy(&vec->items[k].oid, oid); + strbuf_init(&vec->items[k].name, 0); + if (name && *name) + strbuf_addstr(&vec->items[k].name, name); + return; } } @@ -728,7 +748,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) return; table.table_name = vec->dimension_label; - strvec_pushl(&table.header, "Size", "OID", NULL); + strvec_pushl(&table.header, "Size", "OID", "Name", NULL); for (size_t k = 0; k < vec->nr_items; k++) { struct large_item *pk = &vec->items[k]; @@ -736,7 +756,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) strbuf_reset(&size); strbuf_addf(&size, "%"PRIuMAX, (uintmax_t)pk->size); - insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), NULL); + insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), pk->name.buf, NULL); } } strbuf_release(&size); @@ -1198,7 +1218,8 @@ static void increment_object_counts( static void increment_totals(struct survey_context *ctx, struct oid_array *oids, - struct survey_report_object_size_summary *summary) + struct survey_report_object_size_summary *summary, + const char *path) { for (size_t i = 0; i < oids->nr; i++) { struct object_info oi = OBJECT_INFO_INIT; @@ -1235,8 +1256,8 @@ static void increment_totals(struct survey_context *ctx, ctx->report.reachable_objects.commits.parent_cnt_pbin[k]++; base = &ctx->report.reachable_objects.commits.base; - maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents, k, &commit->object.oid); - maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes, object_length, &commit->object.oid); + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents, k, &commit->object.oid, NULL); + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes, object_length, &commit->object.oid, NULL); break; } case OBJ_TREE: { @@ -1256,8 +1277,8 @@ static void increment_totals(struct survey_context *ctx, pst->sum_entries += nr_entries; - maybe_insert_large_item(pst->vec_largest_by_nr_entries, nr_entries, &tree->object.oid); - maybe_insert_large_item(pst->vec_largest_by_size_bytes, object_length, &tree->object.oid); + maybe_insert_large_item(pst->vec_largest_by_nr_entries, nr_entries, &tree->object.oid, path); + maybe_insert_large_item(pst->vec_largest_by_size_bytes, object_length, &tree->object.oid, path); qb = qbin(nr_entries); incr_obj_hist_bin(&pst->entry_qbin[qb], object_length, disk_sizep); @@ -1268,7 +1289,7 @@ static void increment_totals(struct survey_context *ctx, case OBJ_BLOB: base = &ctx->report.reachable_objects.blobs.base; - maybe_insert_large_item(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes, object_length, &oids->oid[i]); + maybe_insert_large_item(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes, object_length, &oids->oid[i], path); break; default: continue; @@ -1308,7 +1329,7 @@ static void increment_object_totals(struct survey_context *ctx, struct survey_report_object_size_summary *total; struct survey_report_object_size_summary summary = { 0 }; - increment_totals(ctx, oids, &summary); + increment_totals(ctx, oids, &summary, path); switch (type) { case OBJ_COMMIT: From 54309939e68c09a2b2e8fac8da3a28e6d08af840 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 15 May 2024 17:44:41 -0400 Subject: [PATCH 011/229] survey: add commit-oid to large_item detail Signed-off-by: Jeff Hostetler --- builtin/survey.c | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index f5ff34273e604e..29632202ed6114 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -156,7 +156,21 @@ static void incr_obj_hist_bin(struct obj_hist_bin *pbin, struct large_item { uint64_t size; struct object_id oid; + + /* + * For blobs and trees the name field is the pathname of the + * file or directory. Root trees will have a zero-length + * name. The name field is not currenly used for commits. + */ struct strbuf name; + + /* + * For blobs and trees remember the transient commit from + * the treewalk so that we can say that this large item + * first appeared in this commit (relative to the treewalk + * order). + */ + struct object_id containing_commit_oid; }; struct large_item_vec { @@ -204,7 +218,8 @@ static void free_large_item_vec(struct large_item_vec *vec) static void maybe_insert_large_item(struct large_item_vec *vec, uint64_t size, struct object_id *oid, - const char *name) + const char *name, + const struct object_id *containing_commit_oid) { size_t rest_len; size_t k; @@ -240,6 +255,7 @@ static void maybe_insert_large_item(struct large_item_vec *vec, memset(&vec->items[k], 0, sizeof(struct large_item)); vec->items[k].size = size; oidcpy(&vec->items[k].oid, oid); + oidcpy(&vec->items[k].containing_commit_oid, containing_commit_oid ? containing_commit_oid : null_oid(the_hash_algo)); strbuf_init(&vec->items[k].name, 0); if (name && *name) strbuf_addstr(&vec->items[k].name, name); @@ -748,7 +764,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) return; table.table_name = vec->dimension_label; - strvec_pushl(&table.header, "Size", "OID", "Name", NULL); + strvec_pushl(&table.header, "Size", "OID", "Name", "Commit", NULL); for (size_t k = 0; k < vec->nr_items; k++) { struct large_item *pk = &vec->items[k]; @@ -756,7 +772,10 @@ static void survey_report_largest_vec(struct large_item_vec *vec) strbuf_reset(&size); strbuf_addf(&size, "%"PRIuMAX, (uintmax_t)pk->size); - insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), pk->name.buf, NULL); + insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), pk->name.buf, + is_null_oid(&pk->containing_commit_oid) ? + "" : oid_to_hex(&pk->containing_commit_oid), + NULL); } } strbuf_release(&size); @@ -1256,8 +1275,8 @@ static void increment_totals(struct survey_context *ctx, ctx->report.reachable_objects.commits.parent_cnt_pbin[k]++; base = &ctx->report.reachable_objects.commits.base; - maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents, k, &commit->object.oid, NULL); - maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes, object_length, &commit->object.oid, NULL); + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents, k, &commit->object.oid, NULL, &commit->object.oid); + maybe_insert_large_item(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes, object_length, &commit->object.oid, NULL, &commit->object.oid); break; } case OBJ_TREE: { @@ -1277,8 +1296,8 @@ static void increment_totals(struct survey_context *ctx, pst->sum_entries += nr_entries; - maybe_insert_large_item(pst->vec_largest_by_nr_entries, nr_entries, &tree->object.oid, path); - maybe_insert_large_item(pst->vec_largest_by_size_bytes, object_length, &tree->object.oid, path); + maybe_insert_large_item(pst->vec_largest_by_nr_entries, nr_entries, &tree->object.oid, path, NULL); + maybe_insert_large_item(pst->vec_largest_by_size_bytes, object_length, &tree->object.oid, path, NULL); qb = qbin(nr_entries); incr_obj_hist_bin(&pst->entry_qbin[qb], object_length, disk_sizep); @@ -1289,7 +1308,7 @@ static void increment_totals(struct survey_context *ctx, case OBJ_BLOB: base = &ctx->report.reachable_objects.blobs.base; - maybe_insert_large_item(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes, object_length, &oids->oid[i], path); + maybe_insert_large_item(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes, object_length, &oids->oid[i], path, NULL); break; default: continue; From 2ff6aa15192fce60ecebcf46c3c11eccb9b8bae5 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 20 May 2024 17:23:39 -0400 Subject: [PATCH 012/229] survey: add commit name-rev lookup to each large_item Signed-off-by: Jeff Hostetler --- builtin/survey.c | 89 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index 29632202ed6114..402468edae7ced 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -13,6 +13,7 @@ #include "ref-filter.h" #include "refs.h" #include "revision.h" +#include "run-command.h" #include "strbuf.h" #include "strvec.h" #include "trace2.h" @@ -171,6 +172,12 @@ struct large_item { * order). */ struct object_id containing_commit_oid; + + /* + * Lookup `containing_commit_oid` using `git name-rev`. + * Lazy allocate this post-treewalk. + */ + struct strbuf name_rev; }; struct large_item_vec { @@ -207,8 +214,10 @@ static void free_large_item_vec(struct large_item_vec *vec) if (!vec) return; - for (size_t k = 0; k < vec->nr_items; k++) + for (size_t k = 0; k < vec->nr_items; k++) { strbuf_release(&vec->items[k].name); + strbuf_release(&vec->items[k].name_rev); + } free(vec->dimension_label); free(vec->item_label); @@ -243,6 +252,9 @@ static void maybe_insert_large_item(struct large_item_vec *vec, * The last large_item in the vector is about to be * overwritten by the previous one during the shift. * Steal its allocated strbuf and reuse it. + * + * We can ignore .name_rev because it will not be + * allocated until after the treewalk. */ strbuf_release(&vec->items[vec->nr_items - 1].name); @@ -764,7 +776,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) return; table.table_name = vec->dimension_label; - strvec_pushl(&table.header, "Size", "OID", "Name", "Commit", NULL); + strvec_pushl(&table.header, "Size", "OID", "Name", "Commit", "Name-Rev", NULL); for (size_t k = 0; k < vec->nr_items; k++) { struct large_item *pk = &vec->items[k]; @@ -775,6 +787,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), pk->name.buf, is_null_oid(&pk->containing_commit_oid) ? "" : oid_to_hex(&pk->containing_commit_oid), + pk->name_rev.len ? pk->name_rev.buf : "", NULL); } } @@ -1126,6 +1139,73 @@ static void do_load_refs(struct survey_context *ctx, ref_sorting_release(sorting); } +/* + * Try to run `git name-rev` on each of the containing-commit-oid's + * in this large-item-vec to get a pretty name for each OID. Silently + * ignore errors if it fails because this info is nice to have but not + * essential. + */ +static void large_item_vec_lookup_name_rev(struct survey_context *ctx, + struct large_item_vec *vec) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf in = STRBUF_INIT; + struct strbuf out = STRBUF_INIT; + const char *line; + size_t k; + + if (!vec || !vec->nr_items) + return; + + ctx->progress_total += vec->nr_items; + display_progress(ctx->progress, ctx->progress_total); + + for (k = 0; k < vec->nr_items; k++) + strbuf_addf(&in, "%s\n", oid_to_hex(&vec->items[k].containing_commit_oid)); + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "name-rev", "--name-only", "--annotate-stdin", NULL); + if (pipe_command(&cp, in.buf, in.len, &out, 0, NULL, 0)) { + strbuf_release(&in); + strbuf_release(&out); + return; + } + + line = out.buf; + k = 0; + while (*line) { + const char *eol = strchrnul(line, '\n'); + + strbuf_init(&vec->items[k].name_rev, 0); + strbuf_add(&vec->items[k].name_rev, line, (eol - line)); + + line = eol + 1; + k++; + } + + strbuf_release(&in); + strbuf_release(&out); +} + +static void do_lookup_name_rev(struct survey_context *ctx) +{ + if (ctx->opts.show_progress) { + ctx->progress_total = 0; + ctx->progress = start_progress(ctx->repo, _("Resolving name-revs..."), 0); + } + + large_item_vec_lookup_name_rev(ctx, ctx->report.reachable_objects.commits.vec_largest_by_nr_parents); + large_item_vec_lookup_name_rev(ctx, ctx->report.reachable_objects.commits.vec_largest_by_size_bytes); + + large_item_vec_lookup_name_rev(ctx, ctx->report.reachable_objects.trees.vec_largest_by_nr_entries); + large_item_vec_lookup_name_rev(ctx, ctx->report.reachable_objects.trees.vec_largest_by_size_bytes); + + large_item_vec_lookup_name_rev(ctx, ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes); + + if (ctx->opts.show_progress) + stop_progress(&ctx->progress); +} + /* * The REFS phase: * @@ -1482,7 +1562,10 @@ static void survey_phase_objects(struct survey_context *ctx) release_revisions(&revs); trace2_region_leave("survey", "phase/objects", ctx->repo); -} + + trace2_region_enter("survey", "phase/namerev", the_repository); + do_lookup_name_rev(ctx); + trace2_region_enter("survey", "phase/namerev", the_repository);} int cmd_survey(int argc, const char **argv, const char *prefix, struct repository *repo) { From 78b124a48027e5390147d4f6fee101122ad2a172 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 Oct 2022 21:46:13 +0200 Subject: [PATCH 013/229] cat_one_file(): make it easy to see that the `size` variable is initialized The large `switch` statement makes it a bit impractical to reason about the code. One of the code paths can technically lead to using `size` without being initialized: if the `t` case is taken and the type name is set to the empty string, we would actually leave `size` unintialized right until we use it. Practically, this cannot happen because the `do_oid_object_info_extended()` function is expected to always populate the `type_name` if asked for. However, it is quite unnecessary to leave the code as unwieldy to reason about: Just initialize the variable to 0 and be done with it. Signed-off-by: Johannes Schindelin --- builtin/cat-file.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index 983ecec837b03b..349358830ae69f 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -106,7 +106,7 @@ static int cat_one_file(int opt, const char *exp_type, const char *obj_name) struct object_id oid; enum object_type type; char *buf; - unsigned long size; + unsigned long size = 0; struct object_context obj_context = {0}; struct object_info oi = OBJECT_INFO_INIT; unsigned flags = OBJECT_INFO_LOOKUP_REPLACE; From 652aa89735caac26117327befd04e12ec9ac8138 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 22:07:45 +0100 Subject: [PATCH 014/229] fsck: avoid using an uninitialized variable In `fsck_commit()`, after counting the authors of a commit, we set the `err` variable either when there was no author, or when there were more than two authors recorded. Then we access the `err` variable to figure out whether we should return early. But if there was exactly one author, that variable is still uninitialized. Let's just initialize the variable. This issue was pointed out by CodeQL. Signed-off-by: Johannes Schindelin --- fsck.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fsck.c b/fsck.c index 341e100d24ece0..958fc17a246bb6 100644 --- a/fsck.c +++ b/fsck.c @@ -926,7 +926,7 @@ static int fsck_commit(const struct object_id *oid, { struct object_id tree_oid, parent_oid; unsigned author_count; - int err; + int err = 0; const char *buffer_begin = buffer; const char *buffer_end = buffer + size; const char *p; From f6368164592910607ee5a731b03826ac1136010f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:27:05 +0100 Subject: [PATCH 015/229] revision: defensive programming On the off chance that `lookup_decoration()` cannot find anything, let `leave_one_treesame_to_parent()` return gracefully instead of crashing. Pointed out by CodeQL. Signed-off-by: Johannes Schindelin --- revision.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/revision.c b/revision.c index cf5e6c1ec9e4e1..c3e782c6df4bb9 100644 --- a/revision.c +++ b/revision.c @@ -3393,6 +3393,9 @@ static int leave_one_treesame_to_parent(struct rev_info *revs, struct commit *co struct commit_list *p; unsigned n; + if (!ts) + return 0; + for (p = commit->parents, n = 0; p; p = p->next, n++) { if (ts->treesame[n]) { if (p->item->object.flags & TMP_MARK) { From e0315ad601a031e5cd4918313666b1dbe2458670 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:35:08 +0100 Subject: [PATCH 016/229] get_parent(): defensive programming CodeQL points out that `lookup_commit_reference()` can return NULL values. Signed-off-by: Johannes Schindelin --- object-name.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/object-name.c b/object-name.c index 766c757042a389..bd74dcec4ffa50 100644 --- a/object-name.c +++ b/object-name.c @@ -1122,7 +1122,7 @@ static enum get_oid_result get_parent(struct repository *r, if (ret) return ret; commit = lookup_commit_reference(r, &oid); - if (repo_parse_commit(r, commit)) + if (!commit || repo_parse_commit(r, commit)) return MISSING_OBJECT; if (!idx) { oidcpy(result, &commit->object.oid); From 4a7f67bc2151d529980c12be063e41e84dfedfe9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:38:02 +0100 Subject: [PATCH 017/229] fetch-pack: defensive programming CodeQL points out that `parse_object()` can return NULL values. Signed-off-by: Johannes Schindelin --- fetch-pack.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fetch-pack.c b/fetch-pack.c index 6e2b70ac5b9ce5..4bd2a33134f9e9 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -158,7 +158,7 @@ static struct commit *deref_without_lazy_fetch(const struct object_id *oid, struct tag *tag = (struct tag *) parse_object(the_repository, oid); - if (!tag->tagged) + if (!tag || !tag->tagged) return NULL; if (mark_tags_complete_and_check_obj_db) tag->object.flags |= COMPLETE; From fd9d010d5805b5173497ec2d8f354678e68ae2bd Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:41:12 +0100 Subject: [PATCH 018/229] unparse_commit(): defensive programming CodeQL points out that `lookup_commit()` can return NULL values. Signed-off-by: Johannes Schindelin --- commit.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commit.c b/commit.c index 16d91b2bfcf291..95a127f41df26b 100644 --- a/commit.c +++ b/commit.c @@ -189,7 +189,7 @@ void unparse_commit(struct repository *r, const struct object_id *oid) { struct commit *c = lookup_commit(r, oid); - if (!c->object.parsed) + if (!c || !c->object.parsed) return; free_commit_list(c->parents); c->parents = NULL; From 0691fe8abfacbbd76c9912c9280894ef729bbfe5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:44:08 +0100 Subject: [PATCH 019/229] verify_commit_graph(): defensive programming CodeQL points out that `lookup_commit()` can return NULL values. Signed-off-by: Johannes Schindelin --- commit-graph.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commit-graph.c b/commit-graph.c index 474454db73d094..f95693ce5c4992 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -2783,6 +2783,11 @@ static int verify_one_commit_graph(struct commit_graph *g, g->hash_algo); graph_commit = lookup_commit(r, &cur_oid); + if (!graph_commit) { + graph_report(_("failed to look up commit %s for commit-graph"), + oid_to_hex(&cur_oid)); + continue; + } odb_commit = (struct commit *)create_object(r, &cur_oid, alloc_commit_node(r)); if (repo_parse_commit_internal(r, odb_commit, 0, 0)) { graph_report(_("failed to parse commit %s from object database for commit-graph"), From 7835772943c10c8aa664410a9bb4def001ae4a7f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:49:59 +0100 Subject: [PATCH 020/229] stash: defensive programming CodeQL points out that `parse_tree_indirect()` can return NULL values. Signed-off-by: Johannes Schindelin --- builtin/stash.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/stash.c b/builtin/stash.c index 948eba06fbccb3..1dec16056e6297 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -348,7 +348,7 @@ static int reset_tree(struct object_id *i_tree, int update, int reset) memset(&opts, 0, sizeof(opts)); tree = parse_tree_indirect(i_tree); - if (parse_tree(tree)) + if (!tree || parse_tree(tree)) return -1; init_tree_desc(t, &tree->object.oid, tree->buffer, tree->size); From bb5031b4b95e6f8a07a3f976d4a7bea3b76f6bd2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:51:59 +0100 Subject: [PATCH 021/229] stash: defensive programming CodeQL points out that `lookup_commit()` can return NULL values. Signed-off-by: Johannes Schindelin --- builtin/stash.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/builtin/stash.c b/builtin/stash.c index 1dec16056e6297..669e640906ac7a 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1461,6 +1461,11 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b goto done; } else { head_commit = lookup_commit(the_repository, &info->b_commit); + if (!head_commit) { + ret = error(_("could not look up commit '%s'"), + oid_to_hex (&info->b_commit)); + goto done; + } } if (!check_changes(ps, include_untracked, &untracked_files)) { From 180bf176efae55850ad654e41bde87f9bab1a463 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:53:40 +0100 Subject: [PATCH 022/229] push: defensive programming CodeQL points out that `branch_get()` can return NULL values. Signed-off-by: Johannes Schindelin --- builtin/push.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/push.c b/builtin/push.c index 5b6cebbb856cfc..06fa926358696d 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -90,7 +90,7 @@ static void refspec_append_mapped(struct refspec *refspec, const char *ref, if (push_default == PUSH_DEFAULT_UPSTREAM && skip_prefix(matched->name, "refs/heads/", &branch_name)) { struct branch *branch = branch_get(branch_name); - if (branch->merge_nr == 1 && branch->merge[0]->src) { + if (branch && branch->merge_nr == 1 && branch->merge[0]->src) { refspec_appendf(refspec, "%s:%s", ref, branch->merge[0]->src); return; From 62a190400beac708b5404866a388485107dd18eb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 22:56:26 +0100 Subject: [PATCH 023/229] load_revindex_from_disk(): avoid accessing uninitialized data The `revindex_size` value is uninitialized in case the function is erroring out, but we want to assign its value. Let's just initialize it. Signed-off-by: Johannes Schindelin --- pack-revindex.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pack-revindex.c b/pack-revindex.c index d0791cc4938fa2..11e193e2fb2a70 100644 --- a/pack-revindex.c +++ b/pack-revindex.c @@ -211,7 +211,7 @@ static int load_revindex_from_disk(const struct git_hash_algo *algo, int fd, ret = 0; struct stat st; void *data = NULL; - size_t revindex_size; + size_t revindex_size = 0; struct revindex_header *hdr; if (git_env_bool(GIT_TEST_REV_INDEX_DIE_ON_DISK, 0)) From 02f3d945ce02f090712c2121e3cb625c7adf1dc2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:55:55 +0100 Subject: [PATCH 024/229] fetch: defensive programming CodeQL points out that `branch_get()` can return NULL values. Signed-off-by: Johannes Schindelin --- builtin/fetch.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index c7ff3480fb1827..6cbbd10693da00 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -552,7 +552,7 @@ static struct ref *get_ref_map(struct remote *remote, if (remote && (remote->fetch.nr || /* Note: has_merge implies non-NULL branch->remote_name */ - (has_merge && !strcmp(branch->remote_name, remote->name)))) { + (has_merge && branch && !strcmp(branch->remote_name, remote->name)))) { for (i = 0; i < remote->fetch.nr; i++) { get_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0); if (remote->fetch.items[i].dst && @@ -570,6 +570,7 @@ static struct ref *get_ref_map(struct remote *remote, * Note: has_merge implies non-NULL branch->remote_name */ if (has_merge && + branch && !strcmp(branch->remote_name, remote->name)) add_merge_config(&ref_map, remote_refs, branch, &tail); } else if (!prefetch) { From beacd12b502aff4c3788476e17c42f99093350f7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 22:58:37 +0100 Subject: [PATCH 025/229] load_pack_mtimes_file(): avoid accessing uninitialized data The `mtimes_size` variable is uninitialzed when the function errors out, yet its value is assigned to another variable. Let's just initialize it. Signed-off-by: Johannes Schindelin --- pack-mtimes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pack-mtimes.c b/pack-mtimes.c index 8e1f2dec0ef62f..8dfe5017dfd267 100644 --- a/pack-mtimes.c +++ b/pack-mtimes.c @@ -28,7 +28,7 @@ static int load_pack_mtimes_file(char *mtimes_file, int fd, ret = 0; struct stat st; uint32_t *data = NULL; - size_t mtimes_size, expected_size; + size_t mtimes_size = 0, expected_size; struct mtimes_header header; fd = git_open(mtimes_file); From 4fe23c9d961eb397d5463f990b81591b1a217dd0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 14 Dec 2022 22:04:59 +0100 Subject: [PATCH 026/229] fetch: silence a CodeQL alert about a local variable's address' use after release As pointed out by CodeQL, it is a potentially dangerous practice to store local variables' addresses in non-local structs. My original intention was to make sure to clear it out after it was used, and before the function returns (which is when the address would go stale). However, I faced too much resistance in the Git project against such patches, there seemed to always be the overwhelming sentiment that the code isn't broken (even if it requires a complex and demanding analysis to wrap one's head around _that_). Therefore, I will be pragmatic and simply ask CodeQL to hold its peace about this issue forever. Signed-off-by: Johannes Schindelin --- builtin/fetch.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/builtin/fetch.c b/builtin/fetch.c index c7ff3480fb1827..b80b5c0ebef5a7 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2582,6 +2582,11 @@ int cmd_fetch(int argc, die(_("must supply remote when using --negotiate-only")); gtransport = prepare_transport(remote, 1); if (gtransport->smart_options) { + /* + * Intentionally assign the address of a local variable + * to a non-local struct's field. + * codeql[cpp/stack-address-escape] + */ gtransport->smart_options->acked_commits = &acked_commits; } else { warning(_("protocol does not support --negotiate-only, exiting")); From fbdf66e221d8ffc6eb731b299f019ac37e6cdfa3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:20:54 +0100 Subject: [PATCH 027/229] submodule: check return value of `submodule_from_path()` As pointed out by CodeQL, it could be NULL and we usually check for that. Signed-off-by: Johannes Schindelin --- builtin/submodule--helper.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index fcd73abe5336a9..3dc75ae7326eef 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1907,6 +1907,9 @@ static int determine_submodule_update_strategy(struct repository *r, const char *val; int ret; + if (!sub) + return error(_("could not retrieve submodule information for path '%s'"), path); + key = xstrfmt("submodule.%s.update", sub->name); if (update) { From 8b53b0c6f2fa64964c73f77108bedef6c05a5197 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:22:50 +0100 Subject: [PATCH 028/229] test-tool repository: check return value of `lookup_commit()` On the off-chance that it's NULL... Signed-off-by: Johannes Schindelin --- t/helper/test-repository.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/t/helper/test-repository.c b/t/helper/test-repository.c index 63c37de33d22f1..05417d8f43dcab 100644 --- a/t/helper/test-repository.c +++ b/t/helper/test-repository.c @@ -27,6 +27,8 @@ static void test_parse_commit_in_graph(const char *gitdir, const char *worktree, repo_set_hash_algo(the_repository, hash_algo_by_ptr(r.hash_algo)); c = lookup_commit(&r, commit_oid); + if (!c) + die("Could not look up %s", oid_to_hex(commit_oid)); if (!parse_commit_in_graph(&r, c)) die("Couldn't parse commit"); From c402925f943396f2880a77f6d863be24e7433eba Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 16 Dec 2022 23:25:01 +0100 Subject: [PATCH 029/229] shallow: handle missing shallow commits gracefully As pointed out by CodeQL, `lookup_commit()` can return NULL. Signed-off-by: Johannes Schindelin --- shallow.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shallow.c b/shallow.c index d9cd4e219cb07d..54d716c2428b20 100644 --- a/shallow.c +++ b/shallow.c @@ -705,7 +705,8 @@ void assign_shallow_commits_to_refs(struct shallow_info *info, for (i = 0; i < nr_shallow; i++) { struct commit *c = lookup_commit(the_repository, &oid[shallow[i]]); - c->object.flags |= BOTTOM; + if (c) + c->object.flags |= BOTTOM; } for (i = 0; i < ref->nr; i++) From 65281cb343fb6b2e5462a1d5ba27821709ec227e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 17 Dec 2022 00:02:33 +0100 Subject: [PATCH 030/229] inherit_tracking(): defensive programming CodeQL points out that `branch_get()` can return NULL values. Signed-off-by: Johannes Schindelin --- branch.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/branch.c b/branch.c index 26be35834718f2..f306156f939955 100644 --- a/branch.c +++ b/branch.c @@ -224,6 +224,8 @@ static int inherit_tracking(struct tracking *tracking, const char *orig_ref) skip_prefix(orig_ref, "refs/heads/", &bare_ref); branch = branch_get(bare_ref); + if (!branch) + BUG("could not get branch for '%s", bare_ref); if (!branch->remote_name) { warning(_("asked to inherit tracking from '%s', but no remote is set"), bare_ref); From 25fdfc6f53f0c603e67ee6f9c8032e1fed68e9d8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 17 Dec 2022 22:21:22 +0100 Subject: [PATCH 031/229] commit-graph: suppress warning about using a stale stack addresses The code is a bit too hard to reason about for CodeQL to figure out whether the `fill_commit_graph_info()` function is at all called after `write_commit_graph()` returns (and hence whether `topo_levels` goes out of context before it is used again). The Git project insists that this is correct (and does not want to make the code more obviously correct), so let's silence CodeQL's complaints in this instance. Signed-off-by: Johannes Schindelin --- commit-graph.c | 1 + 1 file changed, 1 insertion(+) diff --git a/commit-graph.c b/commit-graph.c index 474454db73d094..deda16c847a509 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -2561,6 +2561,7 @@ int write_commit_graph(struct odb_source *source, g = prepare_commit_graph(ctx.r); for (struct commit_graph *chain = g; chain; chain = chain->base_graph) + /* Intentional: codeql[cpp/stack-address-escape] */ g->topo_levels = &topo_levels; if (flags & COMMIT_GRAPH_WRITE_BLOOM_FILTERS) From b361e0f3d217c88340b9cbbd5c8fbabacff95a59 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 9 Aug 2022 12:22:00 +0200 Subject: [PATCH 032/229] codeql: run static analysis as part of CI builds CodeQL is GitHub's native offering of a static code analyzer, and hence integrates with GitHub Actions better than any other static code analyzer. By default, it comes with a large range of "queries" that test for common code patterns that should be avoided. For now, we only target source code written in C, via the `language: cpp` directive. Just in case that other languages should be targeted, too, this GitHub workflow job is set up as a matrix job to make that easier in the future. For full documentation, see https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql Co-authored-by: Pierre Tempel Co-authored-by: Arthur Baars Signed-off-by: Johannes Schindelin --- .github/workflows/codeql.yml | 48 ++++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ ci/install-dependencies.sh | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000000..63380463120126 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: "CodeQL" + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '0 3 * * 1' # Every Monday at 03:00 UTC + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["cpp"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: ci/install-dependencies.sh + if: matrix.language == 'cpp' + env: + jobname: codeql + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended + + - name: Build + if: matrix.language == 'cpp' + run: | + cat /proc/cpuinfo + make -j$(nproc) + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index 0482224716d834..275267f2687af7 100644 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,5 @@ Release/ CMakeSettings.json /contrib/libgit-rs/target /contrib/libgit-sys/target +/.github/codeql/.cache/ +/.github/codeql/codeql-pack.lock.yml diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh index 50628ee2dd6ac7..ce90b0dd656520 100755 --- a/ci/install-dependencies.sh +++ b/ci/install-dependencies.sh @@ -133,7 +133,7 @@ case "$jobname" in ClangFormat) sudo apt-get -q -y install clang-format ;; -StaticAnalysis) +StaticAnalysis|codeql) sudo apt-get -q -y install coccinelle libcurl4-openssl-dev libssl-dev \ libexpat-dev gettext make ;; From b37754e9688494ca7a71fa6fb58254acc7b28f70 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 22 Mar 2023 22:27:56 +0100 Subject: [PATCH 033/229] codeql: publish the sarif file as build artifact In some instances, CodeQL's web UI on github.com leaves questions unanswered. For example, in some alerts it is really necessary to follow the entire "taint flow" to understand why something might be an issue. The alerts for the `cpp/uncontrolled-allocation-size` rule, for example, are all false positives, and only when inspecting the exact flow does it become obvious that one alert wants to point out that the size of a binary patch hunk, which is specified in the patch, is then used to determine how much memory to allocate, which may potentially run out of memory (and is hence just Git doing what it is asked to, and does not need to be changed). To help with those issues, publish the `.sarif` file as part of every workflow run; This allows downloading that file and inspecting it e.g. with the SARIF viewer extension in VS Code (for details, see https://marketplace.visualstudio.com/items?itemName=MS-SarifVSCode.sarif-viewer). Signed-off-by: Johannes Schindelin --- .github/workflows/codeql.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 63380463120126..7dd199057e8ef0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,3 +46,21 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 + with: + upload: False + output: sarif-results + + - name: debug + shell: bash + run: ls -la sarif-results + + - name: publish sarif for debugging + uses: actions/upload-artifact@v4 + with: + name: sarif-results + path: sarif-results + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: sarif-results/cpp.sarif From 3a342c010be1d7372c27cf4e10e53f1dcfe6dba5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 21 Mar 2025 10:25:59 +0000 Subject: [PATCH 034/229] codeql: disable a couple of non-critical queries for now A couple of CodeQL's queries are opinionated in a way that is obviously not shared by Git's source code's state, and apparently intentionally so. For example, the "For loop variable changed in body" query as well as the "No trivial switch statements" one result in too many results that are apparently intentional in Git's source code. Let's not worry about those, then. Also, Git has plenty of instances where variables shadow other variables. Other valid yet not quite critical issues identified by CodeQL include complex conditionals and nested switch statements spanning several pages. We probably want to address these issues at some stage, but they are not as critical as other problems pointed out by CodeQL, so let's silence those queries for now and take care of them at a later stage. Signed-off-by: Johannes Schindelin --- .github/codeql/codeql-config.yml | 131 +++++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 4 +- 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 .github/codeql/codeql-config.yml diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000000000..d2f84f8b98aad5 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,131 @@ +name: "CodeQL config" + +queries: + - uses: security-extended + +query-filters: + - exclude: + # yes, this extra indentation is intentional + # too common in Git's source code + id: cpp/trivial-switch + - exclude: + id: cpp/loop-variable-changed + - exclude: + # we override this locally with a modified version + id: cpp/non-constant-format + - exclude: + # Git does not consider this a problem + id: cpp/irregular-enum-init + - exclude: + # Git has many long functions, this alert would match too many + id: cpp/poorly-documented-function + - exclude: + # In Git, there is a lot of commented-out code + id: cpp/commented-out-code + - exclude: + # While it is true that long switch cases are hard to read and + # validate, Git has way too many for us to allow this query to + # churn out alerts left and right + id: cpp/long-switch + - exclude: + # CodeQL does not expect Git to heed the umask(), but it does + id: cpp/world-writable-file-creation + - exclude: + # Git uses the construct `if () ; else ...` often, to + # avoid an extra indentation level. CodeQL does not like that. + id: cpp/empty-block + - exclude: + # This rule unfortunately triggers some false positives, e.g. + # where Git tries to redact URLs or where Git specifically + # asks for a password upon GIT_SSL_CERT_PASSWORD_PROTECTED. + id: cpp/user-controlled-bypass + - exclude: + # This rule fails to recognize that xmallocz() _specifically_ + # makes room for a trailing NUL, and instead assumes that this + # function behaves like malloc(), which does not. + id: cpp/invalid-pointer-deref + - exclude: + # CodeQL fails to recognize that xmallocz() accounts for the NUL, + # instead assuming malloc() semantics. + id: cpp/no-space-for-terminator + - exclude: + # Git does exchange plain-text passwords via stdin/stdout e.g. + # with helpers in the credential protocol, or in credential-cache. + # This rule, though, assumes that writing to _any_ file descriptor + # is unsafe. + id: cpp/cleartext-storage-file + - exclude: + # When storing the value of the environment variable `PWD` as the + # current directory in absolute_pathdup(), or when allocating memory + # for a binary patch where the size is specified in the patch itself, + # CodeQL assumes that this can lead to a denial of service because + # of an unbounded size, but Git's code works as designed here. + id: cpp/uncontrolled-allocation-size + - exclude: + # lock_repo_for_gc() has admittedly obtuse logic to parse the + # process ID out of the `gc.pid` file, which is correct, but + # due to its construction throws a false positive here. + id: cpp/missing-check-scanf + - exclude: + # discard_cache_entry() overwrites the name in a FLEX_ARRAY struct + # if GIT_TEST_VALIDATE_INDEX_CACHE_ENTRIES is set, which CodeQL fails + # to recognize as valid. + id: cpp/overrun-write + - exclude: + # Since `time_t` can be signed or unsigned, there is unfortunately + # no way to avoid letting this rule report a potential + id: cpp/integer-multiplication-cast-to-long + - exclude: + # There are many, many legitimate code paths in Git where a path is + # constructed from an environment variable, e.g. GIT_DIR. Let's suppress + # this slightly overzealous query. + id: cpp/path-injection + - exclude: + # Git has 99 instances of this at the time of writing :-( + id: cpp/declaration-hides-variable + - exclude: + id: cpp/declaration-hides-parameter + - exclude: + id: cpp/local-variable-hides-global-variable + - exclude: + id: cpp/complex-condition + - exclude: + # Nested, long-winded switch statements are hard to read and hard + # to reason about. Looking at you, `format_commit_one()`. + id: cpp/complex-block + - exclude: + # There are four instances of this at time of writing, all intentional. + # However, it is very easy to introduce unintentional re-use of loop + # variable names, therefore we will most likely want to either change these + # instances or add suppressions. + id: cpp/nested-loops-with-same-variable + - exclude: + # zOMG so many FIXMEs + id: cpp/fixme-comment + - exclude: + # Git assumes quite a bit about the user's control of the current worktree + # Therefore, it kind of assumes that TOCTOU issues are not a thing when + # it comes to files. + id: cpp/toctou-race-condition + - exclude: + # Too many results in Git where the code was, however, intentionally written + # the way it is. + id: cpp/stack-address-escape + - exclude: + id: cpp/inconsistent-null-check + - exclude: + # This would trigger alerts in the functions in `help.c` that want to open + # external programs to show manual pages. + id: cpp/uncontrolled-process-operation + - exclude: + # The code in t/unit-tests/u-ctype.c implicitly exercises the `sane_istest()` + # macro extensively, and CodeQL seems to miss the cast to `(unsigned char)`, + # thereby mistaking the accesses for being past the end of the array (which + # is incorrect). + # + # Ideally, we would exclude test programs from CodeQL anyways, but + # unfortunately there is no Makefile rule in Git's code base to build only + # the production code, and CodeQL's `paths-ignore` directive described at + # https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#specifying-directories-to-scan + # unfortunately is _ignored_ for compiled languages. + id: cpp/overflow-buffer diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7dd199057e8ef0..6174cdc82a69c8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install dependencies run: ci/install-dependencies.sh @@ -36,7 +36,7 @@ jobs: uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - queries: security-extended + config-file: ./.github/codeql/codeql-config.yml - name: Build if: matrix.language == 'cpp' From ababb4a3e2a2c3f624da4504641acb8b8808cc15 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 23 Jul 2025 14:18:38 +0200 Subject: [PATCH 035/229] date: help CodeQL understand that there are no leap-year issues here Signed-off-by: Johannes Schindelin --- date.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/date.c b/date.c index 17a95077cf5450..119e065f3aee5c 100644 --- a/date.c +++ b/date.c @@ -524,14 +524,14 @@ static int set_date(int year, int month, int day, struct tm *now_tm, time_t now, if (year == -1) { if (!now_tm) return 1; - r->tm_year = now_tm->tm_year; + r->tm_year = now_tm->tm_year; // CodeQL [SM03231] justification: Git's custom date parser intentionally handles years without leap year validation } else if (year >= 1970 && year < 2100) r->tm_year = year - 1900; else if (year > 70 && year < 100) r->tm_year = year; else if (year < 38) - r->tm_year = year + 100; + r->tm_year = year + 100; // CodeQL [SM03231] justification: Git's date parser handles century offsets without leap year validation by design else return -1; if (!now_tm) @@ -548,7 +548,7 @@ static int set_date(int year, int month, int day, struct tm *now_tm, time_t now, tm->tm_mon = r->tm_mon; tm->tm_mday = r->tm_mday; if (year != -1) - tm->tm_year = r->tm_year; + tm->tm_year = r->tm_year; // CodeQL [SM03231] justification: Git's date parser copies year values without requiring leap year validation return 0; } return -1; @@ -780,11 +780,11 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt /* Two-digit year? */ if (n == 2 && tm->tm_year < 0) { if (num < 10 && tm->tm_mday >= 0) { - tm->tm_year = num + 100; + tm->tm_year = num + 100; // CodeQL [SM03231] justification: Git's digit parser handles century calculation without leap year validation return n; } if (num >= 70) { - tm->tm_year = num; + tm->tm_year = num; // CodeQL [SM03231] justification: Git's legacy date parser handles two-digit years without leap year validation by design return n; } } @@ -1083,7 +1083,7 @@ static time_t update_tm(struct tm *tm, struct tm *now, time_t sec) if (tm->tm_year < 0) { tm->tm_year = now->tm_year; if (tm->tm_mon > now->tm_mon) - tm->tm_year--; + tm->tm_year--; // CodeQL [SM03231] justification: Git's date parser adjusts year to handle month comparisons without leap year validation } n = mktime(tm) - sec; @@ -1110,9 +1110,9 @@ static void pending_number(struct tm *tm, int *num) if (number > 1969 && number < 2100) tm->tm_year = number - 1900; else if (number > 69 && number < 100) - tm->tm_year = number; + tm->tm_year = number; // CodeQL [SM03231] justification: Git's approxidate parser intentionally assigns years without leap year checks else if (number < 38) - tm->tm_year = 100 + number; + tm->tm_year = 100 + number; // CodeQL [SM03231] justification: Git's approxidate parser handles century calculation without leap year validation /* We screw up for number = 00 ? */ } } @@ -1304,7 +1304,7 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm *num = 0; while (n < 0) { n += 12; - tm->tm_year--; + tm->tm_year--; // CodeQL [SM03231] justification: Git's approxidate parser adjusts years for month calculations without leap year concerns } tm->tm_mon = n; *touched = 1; @@ -1313,7 +1313,7 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm if (match_string(date, "years") >= 4) { update_tm(tm, now, 0); /* fill in date fields if needed */ - tm->tm_year -= *num; + tm->tm_year -= *num; // CodeQL [SM03231] justification: Git's approxidate parser subtracts years without leap year validation by design *num = 0; *touched = 1; return end; From 59a23a25ea6d4dac076f25b82c6752d5d6509a05 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 23 Jul 2025 14:24:47 +0200 Subject: [PATCH 036/229] help: help CodeQL understand that consuming envvars is okay here Signed-off-by: Johannes Schindelin --- builtin/help.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/help.c b/builtin/help.c index c09cbc8912deb6..130b517547d77d 100644 --- a/builtin/help.c +++ b/builtin/help.c @@ -278,7 +278,7 @@ static void exec_woman_emacs(const char *path, const char *page) if (!path) path = "emacsclient"; strbuf_addf(&man_page, "(woman \"%s\")", page); - execlp(path, "emacsclient", "-e", man_page.buf, (char *)NULL); + execlp(path, "emacsclient", "-e", man_page.buf, (char *)NULL); // CodeQL [SM01925] justification: Git's help system safely consumes user-controlled environment variables and paths warning_errno(_("failed to exec '%s'"), path); strbuf_release(&man_page); } @@ -300,7 +300,7 @@ static void exec_man_konqueror(const char *path, const char *page) } else path = "kfmclient"; strbuf_addf(&man_page, "man:%s(1)", page); - execlp(path, filename, "newTab", man_page.buf, (char *)NULL); + execlp(path, filename, "newTab", man_page.buf, (char *)NULL); // CodeQL [SM01925] justification: Git's help system safely consumes user-controlled environment variables and paths warning_errno(_("failed to exec '%s'"), path); strbuf_release(&man_page); } @@ -310,7 +310,7 @@ static void exec_man_man(const char *path, const char *page) { if (!path) path = "man"; - execlp(path, "man", page, (char *)NULL); + execlp(path, "man", page, (char *)NULL); // CodeQL [SM01925] justification: Git's help system safely consumes user-controlled environment variables and paths warning_errno(_("failed to exec '%s'"), path); } From b1d23ad0344e36829322946b7279e2e665939692 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 23 Jul 2025 14:31:46 +0200 Subject: [PATCH 037/229] ctype: help CodeQL understand that `sane_istest()` does not access array past end Signed-off-by: Johannes Schindelin --- t/unit-tests/u-ctype.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/t/unit-tests/u-ctype.c b/t/unit-tests/u-ctype.c index 32e65867cdc28d..51a6c27ca45ef6 100644 --- a/t/unit-tests/u-ctype.c +++ b/t/unit-tests/u-ctype.c @@ -33,70 +33,70 @@ void test_ctype__isspace(void) { - TEST_CHAR_CLASS(isspace, " \n\r\t"); + TEST_CHAR_CLASS(isspace, " \n\r\t"); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isdigit(void) { - TEST_CHAR_CLASS(isdigit, DIGIT); + TEST_CHAR_CLASS(isdigit, DIGIT); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isalpha(void) { - TEST_CHAR_CLASS(isalpha, LOWER UPPER); + TEST_CHAR_CLASS(isalpha, LOWER UPPER); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isalnum(void) { - TEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT); + TEST_CHAR_CLASS(isalnum, LOWER UPPER DIGIT); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__is_glob_special(void) { - TEST_CHAR_CLASS(is_glob_special, "*?[\\"); + TEST_CHAR_CLASS(is_glob_special, "*?[\\"); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__is_regex_special(void) { - TEST_CHAR_CLASS(is_regex_special, "$()*+.?[\\^{|"); + TEST_CHAR_CLASS(is_regex_special, "$()*+.?[\\^{|"); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__is_pathspec_magic(void) { - TEST_CHAR_CLASS(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~"); + TEST_CHAR_CLASS(is_pathspec_magic, "!\"#%&',-/:;<=>@_`~"); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isascii(void) { - TEST_CHAR_CLASS(isascii, ASCII); + TEST_CHAR_CLASS(isascii, ASCII); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__islower(void) { - TEST_CHAR_CLASS(islower, LOWER); + TEST_CHAR_CLASS(islower, LOWER); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isupper(void) { - TEST_CHAR_CLASS(isupper, UPPER); + TEST_CHAR_CLASS(isupper, UPPER); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__iscntrl(void) { - TEST_CHAR_CLASS(iscntrl, CNTRL); + TEST_CHAR_CLASS(iscntrl, CNTRL); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__ispunct(void) { - TEST_CHAR_CLASS(ispunct, PUNCT); + TEST_CHAR_CLASS(ispunct, PUNCT); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isxdigit(void) { - TEST_CHAR_CLASS(isxdigit, DIGIT "abcdefABCDEF"); + TEST_CHAR_CLASS(isxdigit, DIGIT "abcdefABCDEF"); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } void test_ctype__isprint(void) { - TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " "); + TEST_CHAR_CLASS(isprint, LOWER UPPER DIGIT PUNCT " "); // CodeQL [SM01947] justification: Code implicitly exercises sane_istest() macro extensively; CodeQL misses the (unsigned char) cast, mistaking accesses for being past array end } From d62134bfb449be8e8f0f19abbf617a70f2a498b8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 23 Jul 2025 14:35:55 +0200 Subject: [PATCH 038/229] ctype: accommodate for CodeQL misinterpreting the `z` in `mallocz()` Signed-off-by: Johannes Schindelin --- refs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refs.c b/refs.c index 965381367e0e53..deb6e37d6215b0 100644 --- a/refs.c +++ b/refs.c @@ -353,7 +353,7 @@ int refname_is_safe(const char *refname) * For example: refs/foo/../bar is safe but refs/foo/../../bar * is not. */ - buf = xmallocz(restlen); + buf = xmallocz(restlen); // CodeQL [SM01952] justification: CodeQL fails to recognize that xmallocz() accounts for the NUL terminator, instead assuming malloc() semantics result = !normalize_path_copy(buf, rest) && !strcmp(buf, rest); free(buf); return result; From 0b03b3046182e2e80a6c01328ad7eb16a62968bc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 23 Jul 2025 14:47:55 +0200 Subject: [PATCH 039/229] strbuf_read: help with CodeQL misunderstanding that `strbuf_read()` does NUL-terminate correctly Signed-off-by: Johannes Schindelin --- builtin/am.c | 14 +++++++------- builtin/clone.c | 2 +- builtin/commit.c | 2 +- builtin/rebase.c | 4 ++-- bundle.c | 4 ++-- credential.c | 2 +- mailinfo.c | 6 +++--- prompt.c | 2 +- sequencer.c | 8 ++++---- strvec.c | 2 +- submodule.c | 6 +++--- t/helper/test-rot13-filter.c | 6 +++--- wrapper.c | 2 +- wt-status.c | 2 +- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/builtin/am.c b/builtin/am.c index 277c2e7937dcc1..504858b3040852 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -434,20 +434,20 @@ static void am_load(struct am_state *state) } read_state_file(&sb, state, "keep", 1); - if (!strcmp(sb.buf, "t")) + if (!strcmp(sb.buf, "t")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand state->keep = KEEP_TRUE; - else if (!strcmp(sb.buf, "b")) + else if (!strcmp(sb.buf, "b")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand state->keep = KEEP_NON_PATCH; else state->keep = KEEP_FALSE; read_state_file(&sb, state, "messageid", 1); - state->message_id = !strcmp(sb.buf, "t"); + state->message_id = !strcmp(sb.buf, "t"); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand read_state_file(&sb, state, "scissors", 1); - if (!strcmp(sb.buf, "t")) + if (!strcmp(sb.buf, "t")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand state->scissors = SCISSORS_TRUE; - else if (!strcmp(sb.buf, "f")) + else if (!strcmp(sb.buf, "f")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand state->scissors = SCISSORS_FALSE; else state->scissors = SCISSORS_UNSET; @@ -455,12 +455,12 @@ static void am_load(struct am_state *state) read_state_file(&sb, state, "quoted-cr", 1); if (!*sb.buf) state->quoted_cr = quoted_cr_unset; - else if (mailinfo_parse_quoted_cr_action(sb.buf, &state->quoted_cr) != 0) + else if (mailinfo_parse_quoted_cr_action(sb.buf, &state->quoted_cr) != 0) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand die(_("could not parse %s"), am_path(state, "quoted-cr")); read_state_file(&sb, state, "apply-opt", 1); strvec_clear(&state->git_apply_opts); - if (sq_dequote_to_strvec(sb.buf, &state->git_apply_opts) < 0) + if (sq_dequote_to_strvec(sb.buf, &state->git_apply_opts) < 0) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand die(_("could not parse %s"), am_path(state, "apply-opt")); state->rebasing = !!file_exists(am_path(state, "rebasing")); diff --git a/builtin/clone.c b/builtin/clone.c index c990f398ef6f37..b0080ae0585efc 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -120,7 +120,7 @@ static const char *get_repo_path_1(struct strbuf *path, int *is_bundle) continue; len = read_in_full(fd, signature, 8); close(fd); - if (len != 8 || strncmp(signature, "gitdir: ", 8)) + if (len != 8 || strncmp(signature, "gitdir: ", 8)) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand continue; dst = read_gitfile(path->buf); if (dst) { diff --git a/builtin/commit.c b/builtin/commit.c index a7077bacb0fca5..c0fde4db793413 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1875,7 +1875,7 @@ int cmd_commit(int argc, if (!stat(git_path_merge_mode(the_repository), &statbuf)) { if (strbuf_read_file(&sb, git_path_merge_mode(the_repository), 0) < 0) die_errno(_("could not read MERGE_MODE")); - if (!strcmp(sb.buf, "no-ff")) + if (!strcmp(sb.buf, "no-ff")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand allow_fast_forward = 0; } if (allow_fast_forward) diff --git a/builtin/rebase.c b/builtin/rebase.c index c46882818982aa..2fa724375fdda2 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -482,9 +482,9 @@ static int read_basic_state(struct rebase_options *opts) if (!read_oneliner(&buf, state_dir_path("allow_rerere_autoupdate", opts), READ_ONELINER_WARN_MISSING)) return -1; - if (!strcmp(buf.buf, "--rerere-autoupdate")) + if (!strcmp(buf.buf, "--rerere-autoupdate")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand opts->allow_rerere_autoupdate = RERERE_AUTOUPDATE; - else if (!strcmp(buf.buf, "--no-rerere-autoupdate")) + else if (!strcmp(buf.buf, "--no-rerere-autoupdate")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand opts->allow_rerere_autoupdate = RERERE_NOAUTOUPDATE; else warning(_("ignoring invalid allow_rerere_autoupdate: " diff --git a/bundle.c b/bundle.c index 42327f9739cc54..e5c94d941e2a4d 100644 --- a/bundle.c +++ b/bundle.c @@ -66,7 +66,7 @@ static int parse_bundle_signature(struct bundle_header *header, const char *line int i; for (i = 0; i < ARRAY_SIZE(bundle_sigs); i++) { - if (!strcmp(line, bundle_sigs[i].signature)) { + if (!strcmp(line, bundle_sigs[i].signature)) { // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand header->version = bundle_sigs[i].version; return 0; } @@ -82,7 +82,7 @@ int read_bundle_header_fd(int fd, struct bundle_header *header, /* The bundle header begins with the signature */ if (strbuf_getwholeline_fd(&buf, fd, '\n') || - parse_bundle_signature(header, buf.buf)) { + parse_bundle_signature(header, buf.buf)) { // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand if (report_path) error(_("'%s' does not look like a v2 or v3 bundle file"), report_path); diff --git a/credential.c b/credential.c index 2594c0c4229ba0..ac589dae56b7ac 100644 --- a/credential.c +++ b/credential.c @@ -258,7 +258,7 @@ static char *credential_ask_one(const char *what, struct credential *c, strbuf_release(&desc); strbuf_release(&prompt); - return xstrdup(r); + return xstrdup(r); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand } static int credential_getpass(struct repository *r, struct credential *c) diff --git a/mailinfo.c b/mailinfo.c index 99ac596e096e70..550bd185e34b97 100644 --- a/mailinfo.c +++ b/mailinfo.c @@ -1238,11 +1238,11 @@ int mailinfo(struct mailinfo *mi, const char *msg, const char *patch) int mailinfo_parse_quoted_cr_action(const char *actionstr, int *action) { - if (!strcmp(actionstr, "nowarn")) + if (!strcmp(actionstr, "nowarn")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand *action = quoted_cr_nowarn; - else if (!strcmp(actionstr, "warn")) + else if (!strcmp(actionstr, "warn")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand *action = quoted_cr_warn; - else if (!strcmp(actionstr, "strip")) + else if (!strcmp(actionstr, "strip")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand *action = quoted_cr_strip; else return -1; diff --git a/prompt.c b/prompt.c index 706fba2a5030c7..6e7bff016ce709 100644 --- a/prompt.c +++ b/prompt.c @@ -37,7 +37,7 @@ static char *do_askpass(const char *cmd, const char *prompt) return NULL; } - strbuf_setlen(&buffer, strcspn(buffer.buf, "\r\n")); + strbuf_setlen(&buffer, strcspn(buffer.buf, "\r\n")); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand return buffer.buf; } diff --git a/sequencer.c b/sequencer.c index 5476d39ba9b097..7df88360ddb77c 100644 --- a/sequencer.c +++ b/sequencer.c @@ -2960,7 +2960,7 @@ static int have_finished_the_last_pick(void) } } /* If there is only one line then we are done */ - eol = strchr(buf.buf, '\n'); + eol = strchr(buf.buf, '\n'); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand if (!eol || !eol[1]) ret = 1; @@ -3193,9 +3193,9 @@ static int read_populate_opts(struct replay_opts *opts) if (read_oneliner(&buf, rebase_path_allow_rerere_autoupdate(), READ_ONELINER_SKIP_IF_EMPTY)) { - if (!strcmp(buf.buf, "--rerere-autoupdate")) + if (!strcmp(buf.buf, "--rerere-autoupdate")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand opts->allow_rerere_auto = RERERE_AUTOUPDATE; - else if (!strcmp(buf.buf, "--no-rerere-autoupdate")) + else if (!strcmp(buf.buf, "--no-rerere-autoupdate")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand opts->allow_rerere_auto = RERERE_NOAUTOUPDATE; strbuf_reset(&buf); } @@ -3240,7 +3240,7 @@ static int read_populate_opts(struct replay_opts *opts) READ_ONELINER_SKIP_IF_EMPTY)) { const char *p = ctx->current_fixups.buf; ctx->current_fixup_count = 1; - while ((p = strchr(p, '\n'))) { + while ((p = strchr(p, '\n'))) { // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand ctx->current_fixup_count++; p++; } diff --git a/strvec.c b/strvec.c index f8de79f5579b49..6f0ec491a4d05a 100644 --- a/strvec.c +++ b/strvec.c @@ -22,7 +22,7 @@ void strvec_push_nodup(struct strvec *array, char *value) const char *strvec_push(struct strvec *array, const char *value) { - strvec_push_nodup(array, xstrdup(value)); + strvec_push_nodup(array, xstrdup(value)); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand return array->v[array->nr - 1]; } diff --git a/submodule.c b/submodule.c index 35c55155f7bf83..c5d3f71197320d 100644 --- a/submodule.c +++ b/submodule.c @@ -2512,11 +2512,11 @@ int get_superproject_working_tree(struct strbuf *buf) * The format is SP SP TAB \0, * We're only interested in the name after the tab. */ - super_sub = strchr(sb.buf, '\t') + 1; - super_sub_len = strlen(super_sub); + super_sub = strchr(sb.buf, '\t') + 1; // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand + super_sub_len = strlen(super_sub); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand if (super_sub_len > cwd_len || - strcmp(&cwd[cwd_len - super_sub_len], super_sub)) + strcmp(&cwd[cwd_len - super_sub_len], super_sub)) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand BUG("returned path string doesn't match cwd?"); super_wt = xstrdup(cwd); diff --git a/t/helper/test-rot13-filter.c b/t/helper/test-rot13-filter.c index ad37e1003445aa..b874992196e91b 100644 --- a/t/helper/test-rot13-filter.c +++ b/t/helper/test-rot13-filter.c @@ -215,7 +215,7 @@ static void command_loop(void) /* Read until flush */ while ((buf = packet_read_line(0, NULL))) { - if (!strcmp(buf, "can-delay=1")) { + if (!strcmp(buf, "can-delay=1")) { // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand entry = strmap_get(&delay, pathname); if (entry && !entry->requested) entry->requested = 1; @@ -308,11 +308,11 @@ static void packet_initialize(void) { char *pkt_buf = packet_read_line(0, NULL); - if (!pkt_buf || strcmp(pkt_buf, "git-filter-client")) + if (!pkt_buf || strcmp(pkt_buf, "git-filter-client")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand die("bad initialize: '%s'", str_or_null(pkt_buf)); pkt_buf = packet_read_line(0, NULL); - if (!pkt_buf || strcmp(pkt_buf, "version=2")) + if (!pkt_buf || strcmp(pkt_buf, "version=2")) // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand die("bad version: '%s'", str_or_null(pkt_buf)); pkt_buf = packet_read_line(0, NULL); diff --git a/wrapper.c b/wrapper.c index 3d507d42045203..4e0ae1ff534f1d 100644 --- a/wrapper.c +++ b/wrapper.c @@ -40,7 +40,7 @@ static int memory_limit_check(size_t size, int gentle) char *xstrdup(const char *str) { - char *ret = strdup(str); + char *ret = strdup(str); // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand if (!ret) die("Out of memory, strdup failed"); return ret; diff --git a/wt-status.c b/wt-status.c index c9e14101424991..a7df2ec2b5d304 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1111,7 +1111,7 @@ size_t wt_status_locate_end(const char *s, size_t len) if (starts_with(s, pattern.buf + 1) && starts_with_newline(s + pattern.len - 1)) len = 0; - else if ((p = strstr(s, pattern.buf)) && + else if ((p = strstr(s, pattern.buf)) && // CodeQL [SM01932] justification: CodeQL is wrong here because the value is read from a file via strbuf_read() which does NUL-terminate the string, something CodeQL fails to understand starts_with_newline(p + pattern.len)) { size_t newlen = p - s + 1; if (newlen < len) From 38820bf4d5c18558949387c8ab5eda67c53fe929 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 24 Jul 2025 14:25:47 +0200 Subject: [PATCH 040/229] codeql: also check JavaScript code Let's exclude GitWeb from being scanned; It is not distributed by us. Signed-off-by: Johannes Schindelin --- .github/codeql/codeql-config.yml | 3 +++ .github/workflows/codeql.yml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index d2f84f8b98aad5..d9ae1e8487a09d 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -3,6 +3,9 @@ name: "CodeQL config" queries: - uses: security-extended +paths-ignore: + - gitweb/**/*.js # GitWeb is not distributed + query-filters: - exclude: # yes, this extra indentation is intentional diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6174cdc82a69c8..0d3e51910250ec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - language: ["cpp"] + language: ["cpp", "javascript"] steps: - name: Checkout repository @@ -57,10 +57,10 @@ jobs: - name: publish sarif for debugging uses: actions/upload-artifact@v4 with: - name: sarif-results + name: sarif-results-${{ matrix.language }} path: sarif-results - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: sarif-results/cpp.sarif + sarif_file: sarif-results/${{ matrix.language }}.sarif From 0e5310ef33409ecf0aba3f22a800a004c8e21e1a Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 4 Jun 2024 10:37:23 -0400 Subject: [PATCH 041/229] survey: add --no-name-rev option Computing `git name-rev` on each commit, tree, and blob in each of the various large_item_vec can be very expensive if there are too many refs, especially if the user doesn't need the result. Lets make it optional. The `--no-name-rev` option can save 50 calls to `git name-rev` since we have 5 large_item_vec's and each defaults to 10 items. Signed-off-by: Jeff Hostetler --- Documentation/config/survey.adoc | 4 ++++ Documentation/git-survey.adoc | 5 +++++ builtin/survey.c | 38 +++++++++++++++++++++++--------- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Documentation/config/survey.adoc b/Documentation/config/survey.adoc index fd2d7e153b38ce..f3ae768933fe1b 100644 --- a/Documentation/config/survey.adoc +++ b/Documentation/config/survey.adoc @@ -4,6 +4,10 @@ survey.*:: background with these options. + -- + survey.namerev:: + Boolean to show/hide `git name-rev` information for each + reported commit and the containing commit of each + reported tree and blob. verbose:: This boolean value implies the `--[no-]verbose` option. progress:: diff --git a/Documentation/git-survey.adoc b/Documentation/git-survey.adoc index f73f1706b6526f..e75030d0067409 100644 --- a/Documentation/git-survey.adoc +++ b/Documentation/git-survey.adoc @@ -32,6 +32,11 @@ OPTIONS --progress:: Show progress. This is automatically enabled when interactive. +--name-rev:: +--no-name-rev:: + Print `git name-rev` output for each commit, tree, and blob. + Defaults to true. + Ref Selection ~~~~~~~~~~~~~ diff --git a/builtin/survey.c b/builtin/survey.c index 402468edae7ced..d248ddef0b1503 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -42,6 +42,7 @@ static struct survey_refs_wanted default_ref_options = { struct survey_opts { int verbose; int show_progress; + int show_name_rev; int show_largest_commits_by_nr_parents; int show_largest_commits_by_size_bytes; @@ -767,7 +768,7 @@ static void survey_report_commit_parents(struct survey_context *ctx) clear_table(&table); } -static void survey_report_largest_vec(struct large_item_vec *vec) +static void survey_report_largest_vec(struct survey_context *ctx, struct large_item_vec *vec) { struct survey_table table = SURVEY_TABLE_INIT; struct strbuf size = STRBUF_INIT; @@ -776,7 +777,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) return; table.table_name = vec->dimension_label; - strvec_pushl(&table.header, "Size", "OID", "Name", "Commit", "Name-Rev", NULL); + strvec_pushl(&table.header, "Size", "OID", "Name", "Commit", ctx->opts.show_name_rev ? "Name-Rev" : NULL, NULL); for (size_t k = 0; k < vec->nr_items; k++) { struct large_item *pk = &vec->items[k]; @@ -787,7 +788,7 @@ static void survey_report_largest_vec(struct large_item_vec *vec) insert_table_rowv(&table, size.buf, oid_to_hex(&pk->oid), pk->name.buf, is_null_oid(&pk->containing_commit_oid) ? "" : oid_to_hex(&pk->containing_commit_oid), - pk->name_rev.len ? pk->name_rev.buf : "", + !ctx->opts.show_name_rev ? NULL : pk->name_rev.len ? pk->name_rev.buf : "", NULL); } } @@ -977,11 +978,11 @@ static void survey_report_plaintext(struct survey_context *ctx) survey_report_plaintext_sorted_size( &ctx->report.top_paths_by_inflate[REPORT_TYPE_BLOB]); - survey_report_largest_vec(ctx->report.reachable_objects.commits.vec_largest_by_nr_parents); - survey_report_largest_vec(ctx->report.reachable_objects.commits.vec_largest_by_size_bytes); - survey_report_largest_vec(ctx->report.reachable_objects.trees.vec_largest_by_nr_entries); - survey_report_largest_vec(ctx->report.reachable_objects.trees.vec_largest_by_size_bytes); - survey_report_largest_vec(ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes); + survey_report_largest_vec(ctx, ctx->report.reachable_objects.commits.vec_largest_by_nr_parents); + survey_report_largest_vec(ctx, ctx->report.reachable_objects.commits.vec_largest_by_size_bytes); + survey_report_largest_vec(ctx, ctx->report.reachable_objects.trees.vec_largest_by_nr_entries); + survey_report_largest_vec(ctx, ctx->report.reachable_objects.trees.vec_largest_by_size_bytes); + survey_report_largest_vec(ctx, ctx->report.reachable_objects.blobs.vec_largest_by_size_bytes); } /* @@ -1053,6 +1054,10 @@ static int survey_load_config_cb(const char *var, const char *value, ctx->opts.show_progress = git_config_bool(var, value); return 0; } + if (!strcmp(var, "survey.namerev")) { + ctx->opts.show_name_rev = git_config_bool(var, value); + return 0; + } if (!strcmp(var, "survey.showcommitparents")) { ctx->opts.show_largest_commits_by_nr_parents = git_config_ulong(var, value, cctx->kvi); return 0; @@ -1189,6 +1194,13 @@ static void large_item_vec_lookup_name_rev(struct survey_context *ctx, static void do_lookup_name_rev(struct survey_context *ctx) { + /* + * `git name-rev` can be very expensive when there are lots of + * refs, so make it optional. + */ + if (!ctx->opts.show_name_rev) + return; + if (ctx->opts.show_progress) { ctx->progress_total = 0; ctx->progress = start_progress(ctx->repo, _("Resolving name-revs..."), 0); @@ -1563,9 +1575,12 @@ static void survey_phase_objects(struct survey_context *ctx) release_revisions(&revs); trace2_region_leave("survey", "phase/objects", ctx->repo); - trace2_region_enter("survey", "phase/namerev", the_repository); - do_lookup_name_rev(ctx); - trace2_region_enter("survey", "phase/namerev", the_repository);} + if (ctx->opts.show_name_rev) { + trace2_region_enter("survey", "phase/namerev", the_repository); + do_lookup_name_rev(ctx); + trace2_region_enter("survey", "phase/namerev", the_repository); + } +} int cmd_survey(int argc, const char **argv, const char *prefix, struct repository *repo) { @@ -1589,6 +1604,7 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor static struct option survey_options[] = { OPT__VERBOSE(&ctx.opts.verbose, N_("verbose output")), OPT_BOOL(0, "progress", &ctx.opts.show_progress, N_("show progress")), + OPT_BOOL(0, "name-rev", &ctx.opts.show_name_rev, N_("run name-rev on each reported commit")), OPT_INTEGER('n', "top", &ctx.opts.top_nr, N_("number of entries to include in detail tables")), From b38a286d908ce7d47de592bcf03ce978a546a26e Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 17 Jun 2024 15:20:05 -0400 Subject: [PATCH 042/229] survey: started TODO list at bottom of source file --- builtin/survey.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/builtin/survey.c b/builtin/survey.c index d248ddef0b1503..045a2937f0b70e 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -1689,3 +1689,49 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor clear_survey_context(&ctx); return 0; } + +/* + * NEEDSWORK: The following is a bit of a laundry list of things + * that I'd like to add. + * + * [] Dump stats on all of the packfiles. The number and size of each. + * Whether each is in the .git directory or in an alternate. The state + * of the IDX or MIDX files and etc. Delta chain stats. All of this + * data is relative to the "lived-in" state of the repository. Stuff + * that may change after a GC or repack. + * + * [] Dump stats on each remote. When we fetch from a remote the size + * of the response is related to the set of haves on the server. You + * can see this in `GIT_TRACE_CURL=1 git fetch`. We get a `ls-refs` + * payload that lists all of the branches and tags on the server, so + * at a minimum the RefName and SHA for each. But for annotated tags + * we also get the peeled SHA. The size of this overhead on every + * fetch is proporational to the size of the `git ls-remote` response + * (roughly, although the latter repeats the RefName of the peeled + * tag). If, for example, you have 500K refs on a remote, you're + * going to have a long "haves" message, so every fetch will be slow + * just because of that overhead (not counting new objects to be + * downloaded). + * + * Note that the local set of tags in "refs/tags/" is a union over all + * remotes. However, since most people only have one remote, we can + * probaly estimate the overhead value directly from the size of the + * set of "refs/tags/" that we visited while building the `ref_info` + * and `ref_array` and not need to ask the remote. + * + * [] Dump info on the complexity of the DAG. Criss-cross merges. + * The number of edges that must be touched to compute merge bases. + * Edge length. The number of parallel lanes in the history that must + * be navigated to get to the merge base. What affects the cost of + * the Ahead/Behind computation? How often do criss-crosses occur and + * do they cause various operations to slow down? + * + * [] If there are primary branches (like "main" or "master") are they + * always on the left side of merges? Does the graph have a clean + * left edge? Or are there normal and "backwards" merges? Do these + * cause problems at scale? + * + * [] If we have a hierarchy of FI/RI branches like "L1", "L2, ..., + * can we learn anything about the shape of the repo around these FI + * and RI integrations? + */ From 8bc641017e6ed7df41fb210eb37980015ed2ff36 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 28 Jun 2024 15:22:46 -0400 Subject: [PATCH 043/229] survey: expanded TODO list at the bottom of the source file Signed-off-by: Jeff Hostetler --- builtin/survey.c | 148 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/builtin/survey.c b/builtin/survey.c index 045a2937f0b70e..4abb27e041b06a 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -1691,47 +1691,131 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor } /* - * NEEDSWORK: The following is a bit of a laundry list of things - * that I'd like to add. + * NEEDSWORK: So far, I only have iteration on the requested set of + * refs and treewalk/reachable objects on that set of refs. The + * following is a bit of a laundry list of things that I'd like to + * add. * * [] Dump stats on all of the packfiles. The number and size of each. - * Whether each is in the .git directory or in an alternate. The state - * of the IDX or MIDX files and etc. Delta chain stats. All of this - * data is relative to the "lived-in" state of the repository. Stuff - * that may change after a GC or repack. + * Whether each is in the .git directory or in an alternate. The + * state of the IDX or MIDX files and etc. Delta chain stats. All + * of this data is relative to the "lived-in" state of the + * repository. Stuff that may change after a GC or repack. + * + * [] Clone and Index stats. partial, shallow, sparse-checkout, + * sparse-index, etc. Hydration stats. * * [] Dump stats on each remote. When we fetch from a remote the size - * of the response is related to the set of haves on the server. You - * can see this in `GIT_TRACE_CURL=1 git fetch`. We get a `ls-refs` - * payload that lists all of the branches and tags on the server, so - * at a minimum the RefName and SHA for each. But for annotated tags - * we also get the peeled SHA. The size of this overhead on every - * fetch is proporational to the size of the `git ls-remote` response - * (roughly, although the latter repeats the RefName of the peeled - * tag). If, for example, you have 500K refs on a remote, you're - * going to have a long "haves" message, so every fetch will be slow - * just because of that overhead (not counting new objects to be - * downloaded). + * of the response is related to the set of haves on the server. + * You can see this in `GIT_TRACE_CURL=1 git fetch`. We get a + * `ls-refs` payload that lists all of the branches and tags on the + * server, so at a minimum the RefName and SHA for each. But for + * annotated tags we also get the peeled SHA. The size of this + * overhead on every fetch is proporational to the size of the `git + * ls-remote` response (roughly, although the latter repeats the + * RefName of the peeled tag). If, for example, you have 500K refs + * on a remote, you're going to have a long "haves" message, so + * every fetch will be slow just because of that overhead (not + * counting new objects to be downloaded). * - * Note that the local set of tags in "refs/tags/" is a union over all - * remotes. However, since most people only have one remote, we can - * probaly estimate the overhead value directly from the size of the - * set of "refs/tags/" that we visited while building the `ref_info` - * and `ref_array` and not need to ask the remote. + * Note that the local set of tags in "refs/tags/" is a union over + * all remotes. However, since most people only have one remote, + * we can probaly estimate the overhead value directly from the + * size of the set of "refs/tags/" that we visited while building + * the `ref_info` and `ref_array` and not need to ask the remote. * * [] Dump info on the complexity of the DAG. Criss-cross merges. - * The number of edges that must be touched to compute merge bases. - * Edge length. The number of parallel lanes in the history that must - * be navigated to get to the merge base. What affects the cost of - * the Ahead/Behind computation? How often do criss-crosses occur and - * do they cause various operations to slow down? + * The number of edges that must be touched to compute merge bases. + * Edge length. The number of parallel lanes in the history that + * must be navigated to get to the merge base. What affects the + * cost of the Ahead/Behind computation? How often do + * criss-crosses occur and do they cause various operations to slow + * down? * * [] If there are primary branches (like "main" or "master") are they - * always on the left side of merges? Does the graph have a clean - * left edge? Or are there normal and "backwards" merges? Do these - * cause problems at scale? + * always on the left side of merges? Does the graph have a clean + * left edge? Or are there normal and "backwards" merges? Do + * these cause problems at scale? * * [] If we have a hierarchy of FI/RI branches like "L1", "L2, ..., - * can we learn anything about the shape of the repo around these FI - * and RI integrations? + * can we learn anything about the shape of the repo around these + * FI and RI integrations? + * + * [] Do we need a no-PII flag to omit pathnames or branch/tag names + * in the various histograms? (This would turn off --name-rev + * too.) + * + * [] I have so far avoided adding opinions about individual fields + * (such as the way `git-sizer` prints a row of stars or bangs in + * the last column). + * + * I'm wondering if that is a job of this executable or if it + * should be done in a post-processing step using the JSON output. + * + * My problem with the `git-sizer` approach is that it doesn't give + * the (casual) user any information on why it has stars or bangs. + * And there isn't a good way to print detailed information in the + * ASCII-art tables that would be easy to understand. + * + * [] For example, a large number of refs does not define a cliff. + * Performance will drop off (linearly, quadratically, ... ??). + * The tool should refer them to article(s) talking about the + * different problems that it could cause. So should `git + * survey` just print the number and (implicitly) refer them to + * the man page (chapter/verse) or to a tool that will interpret + * the number and explain it? + * + * [] Alternatively, should `git survey` do that analysis too and + * just print footnotes for each large number? + * + * [] The computation of the raw survey JSON data can take HOURS on + * a very large repo (like Windows), so I'm wondering if we + * want to keep the opinion portion separate. + * + * [] In addition to opinions based on the static data, I would like + * to dump the JSON results (or the Trace2 telemetry) into a DB and + * aggregate it with other users. + * + * Granted, they should all see the same DAG and the same set of + * reachable objects, but we could average across all datasets + * generated on a particular date and detect outlier users. + * + * [] Maybe someone cloned from the `_full` endpoint rather than + * the limited refs endpoint. + * + * [] Maybe that user is having problems with repacking / GC / + * maintenance without knowing it. + * + * [] I'd also like to dump use the DB to compare survey datasets over + * a time. How fast is their repository growing and in what ways? + * + * [] I'd rather have the delta analysis NOT be inside `git + * survey`, so it makes sense to consider having all of it in a + * post-process step. + * + * [] Another reason to put the opinion analysis in a post-process + * is that it would be easier to generate plots on the data tables. + * Granted, we can get plots from telemetry, but a stand-alone user + * could run the JSON thru python or jq or something and generate + * something nicer than ASCII-art and it could handle cross-referencing + * and hyperlinking to helpful information on each issue. + * + * [] I think there are several classes of data that we can report on: + * + * [] The "inherit repo properties", such as the shape and size of + * the DAG -- these should be universal in each enlistment. + * + * [] The "ODB lived in properties", such as the efficiency + * of the repack and things like partial and shallow clone. + * These will vary, but indicate health of the ODB. + * + * [] The "index related properties", such as sparse-checkout, + * sparse-index, cache-tree, untracked-cache, fsmonitor, and + * etc. These will also vary, but are more like knobs for + * the user to adjust. + * + * [] I want to compare these with Matt's "dimensions of scale" + * notes and see if there are other pieces of data that we + * could compute/consider. + * */ From 3ac44517e54fc07ef7f826598b3681b8d942695c Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 1 Jul 2024 12:07:01 -0400 Subject: [PATCH 044/229] survey: expanded TODO with more notes Signed-off-by: Jeff Hostetler --- builtin/survey.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/builtin/survey.c b/builtin/survey.c index 4abb27e041b06a..53a7b08f015a2b 100644 --- a/builtin/survey.c +++ b/builtin/survey.c @@ -1724,6 +1724,16 @@ int cmd_survey(int argc, const char **argv, const char *prefix, struct repositor * size of the set of "refs/tags/" that we visited while building * the `ref_info` and `ref_array` and not need to ask the remote. * + * [] Should the "string length of refnames / remote refs", for + * example, be sub-divided by remote so we can project the + * cost of the haves/wants overhead a fetch. + * + * [] Can we examine the merge commits and classify them as clean or + * dirty? (ie. ones with merge conflicts that needed to be + * addressed during the merge itself.) + * + * [] Do dirty merges affect performance of later operations? + * * [] Dump info on the complexity of the DAG. Criss-cross merges. * The number of edges that must be touched to compute merge bases. * Edge length. The number of parallel lanes in the history that From 45ebce6c578d5eea43ca0b074634d69cc2f0176d Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Wed, 5 Apr 2017 10:58:09 -0600 Subject: [PATCH 045/229] reset --stdin: trim carriage return from the paths While using the reset --stdin feature on windows path added may have a \r at the end of the path that wasn't getting removed so didn't match the path in the index and wasn't reset. Signed-off-by: Kevin Willford --- t/t7108-reset-stdin.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/t/t7108-reset-stdin.sh b/t/t7108-reset-stdin.sh index b7cbcbf869296c..db5483b8f10052 100755 --- a/t/t7108-reset-stdin.sh +++ b/t/t7108-reset-stdin.sh @@ -29,4 +29,13 @@ test_expect_success '--stdin requires --mixed' ' git reset --mixed --stdin list && + git reset --stdin Date: Tue, 23 Apr 2024 12:21:01 +0200 Subject: [PATCH 046/229] Identify microsoft/git via a distinct version suffix It has been a long-standing practice in Git for Windows to append `.windows.`, and in microsoft/git to append `.vfs.0.0`. Let's keep doing that. Signed-off-by: Johannes Schindelin --- GIT-VERSION-GEN | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index c43f33d8893153..cad2bf4dd2a244 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -2,6 +2,9 @@ DEF_VER=v2.52.0-rc0 +# Identify microsoft/git via a distinct version suffix +DEF_VER=$DEF_VER.vfs.0.0 + LF=' ' From a94c4d4f3afa3ae8f93d24a585d3a9590c5d28c2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 4 Apr 2017 12:04:11 +0200 Subject: [PATCH 047/229] gvfs: ensure that the version is based on a GVFS tag Since we really want to be based on a `.vfs.*` tag, let's make sure that there was a new-enough one, i.e. one that agrees with the first three version numbers of the recorded default version. This prevents e.g. v2.22.0.vfs.0.. from being used when the current release train was not yet tagged. It is important to get the first three numbers of the version right because e.g. Scalar makes decisions depending on those (such as assuming that the `git maintenance` built-in is not available, even though it actually _is_ available). Signed-off-by: Johannes Schindelin --- GIT-VERSION-GEN | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index cad2bf4dd2a244..313ef6ceaa6c9e 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -50,9 +50,15 @@ then test -d "${GIT_DIR:-.git}" || test -f "$SOURCE_DIR"/.git; } && - VN=$(git -C "$SOURCE_DIR" describe --dirty --match="v[0-9]*" 2>/dev/null) && + VN=$(git -C "$SOURCE_DIR" describe --dirty --match="v[0-9]*vfs*" 2>/dev/null) && case "$VN" in *$LF*) (exit 1) ;; + v[0-9]*) + if test "${VN%%.vfs.*}" != "${DEF_VER%%.vfs.*}" + then + echo "Found version $VN, which is not based on $DEF_VER" >&2 + exit 1 + fi ;; esac then VN=$(echo "$VN" | sed -e 's/-/./g'); From c88546c7eaf834cffc2bb154b87c91cfba880931 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Tue, 24 Jan 2017 17:30:59 +0100 Subject: [PATCH 048/229] gvfs: add a GVFS-specific header file This header file will accumulate GVFS-specific definitions. Signed-off-by: Kevin Willford --- gvfs.h | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 gvfs.h diff --git a/gvfs.h b/gvfs.h new file mode 100644 index 00000000000000..b6dbe85eae4071 --- /dev/null +++ b/gvfs.h @@ -0,0 +1,9 @@ +#ifndef GVFS_H +#define GVFS_H + +/* + * This file is for the specific settings and methods + * used for GVFS functionality + */ + +#endif /* GVFS_H */ From a233cc2235f26083a62a6dcf0c01953e89d18025 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Tue, 24 Jan 2017 17:34:12 +0100 Subject: [PATCH 049/229] gvfs: add the core.gvfs config setting This does not do anything yet. The next patches will add various values for that config setting that correspond to the various features offered/required by GVFS. Signed-off-by: Kevin Willford gvfs: refactor loading the core.gvfs config value This code change makes sure that the config value for core_gvfs is always loaded before checking it. Signed-off-by: Kevin Willford --- Documentation/config/core.adoc | 3 +++ Makefile | 1 + config.c | 1 + gvfs.c | 48 ++++++++++++++++++++++++++++++++++ gvfs.h | 4 +++ meson.build | 1 + 6 files changed, 58 insertions(+) create mode 100644 gvfs.c diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 34c44d2c8f8725..bfcf29b85b9e11 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -761,6 +761,9 @@ core.multiPackIndex:: single index. See linkgit:git-multi-pack-index[1] for more information. Defaults to true. +core.gvfs:: + Enable the features needed for GVFS. + core.sparseCheckout:: Enable "sparse checkout" feature. See linkgit:git-sparse-checkout[1] for more information. diff --git a/Makefile b/Makefile index 09997cbc09c3ea..604ae7bd63ef0a 100644 --- a/Makefile +++ b/Makefile @@ -1162,6 +1162,7 @@ LIB_OBJS += git-zlib.o LIB_OBJS += gpg-interface.o LIB_OBJS += graph.o LIB_OBJS += grep.o +LIB_OBJS += gvfs.o LIB_OBJS += hash-lookup.o LIB_OBJS += hash.o LIB_OBJS += hashmap.o diff --git a/config.c b/config.c index 71b136bf7f6ec0..9abda809f69e20 100644 --- a/config.c +++ b/config.c @@ -10,6 +10,7 @@ #include "abspath.h" #include "advice.h" #include "date.h" +#include "gvfs.h" #include "branch.h" #include "config.h" #include "dir.h" diff --git a/gvfs.c b/gvfs.c new file mode 100644 index 00000000000000..0c43639bcb21de --- /dev/null +++ b/gvfs.c @@ -0,0 +1,48 @@ +#define USE_THE_REPOSITORY_VARIABLE +#include "git-compat-util.h" +#include "environment.h" +#include "gvfs.h" +#include "setup.h" +#include "config.h" + +static int gvfs_config_loaded; +static struct repository *gvfs_repo; +static int core_gvfs; +static int core_gvfs_is_bool; + +static int early_core_gvfs_config(const char *var, const char *value, + const struct config_context *ctx, void *cb UNUSED) +{ + if (!strcmp(var, "core.gvfs")) + core_gvfs = git_config_bool_or_int("core.gvfs", value, ctx->kvi, + &core_gvfs_is_bool); + return 0; +} + +static void gvfs_load_config_value(struct repository *r) +{ + if (gvfs_config_loaded && gvfs_repo == r) + return; + + if (r) { + repo_config_get_bool_or_int(r, "core.gvfs", + &core_gvfs_is_bool, &core_gvfs); + } else if (startup_info->have_repository == 0) + read_early_config(the_repository, early_core_gvfs_config, NULL); + else + repo_config_get_bool_or_int(the_repository, "core.gvfs", + &core_gvfs_is_bool, &core_gvfs); + + /* Turn on all bits if a bool was set in the settings */ + if (core_gvfs_is_bool && core_gvfs) + core_gvfs = -1; + + gvfs_config_loaded = 1; + gvfs_repo = r; +} + +int gvfs_config_is_set(struct repository *r, int mask) +{ + gvfs_load_config_value(r); + return (core_gvfs & mask) == mask; +} diff --git a/gvfs.h b/gvfs.h index b6dbe85eae4071..01b748ebcecc99 100644 --- a/gvfs.h +++ b/gvfs.h @@ -1,9 +1,13 @@ #ifndef GVFS_H #define GVFS_H +struct repository; + /* * This file is for the specific settings and methods * used for GVFS functionality */ +int gvfs_config_is_set(struct repository *r, int mask); + #endif /* GVFS_H */ diff --git a/meson.build b/meson.build index 4f657a45d140b2..9be1056fc176ae 100644 --- a/meson.build +++ b/meson.build @@ -352,6 +352,7 @@ libgit_sources = [ 'gpg-interface.c', 'graph.c', 'grep.c', + 'gvfs.c', 'hash-lookup.c', 'hash.c', 'hashmap.c', From 96c16e4fc99572426f0210f62beb35ee03433fae Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Tue, 24 Jan 2017 17:38:59 +0100 Subject: [PATCH 050/229] gvfs: add the feature to skip writing the index' SHA-1 This takes a substantial amount of time, and if the user is reasonably sure that the files' integrity is not compromised, that time can be saved. Git no longer verifies the SHA-1 by default, anyway. Signed-off-by: Kevin Willford Update for 2023-02-27: This feature was upstreamed as the index.skipHash config option. This resulted in some changes to the struct and some of the setup code. In particular, the config reading was moved to prepare_repo_settings(), so the core.gvfs bit check was moved there, too. Signed-off-by: Kevin Willford Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- Documentation/config/core.adoc | 10 +++++++++- gvfs.h | 5 +++++ repo-settings.c | 8 ++++++++ t/meson.build | 1 + t/t1017-read-tree-skip-sha-on-read.sh | 22 ++++++++++++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100755 t/t1017-read-tree-skip-sha-on-read.sh diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index bfcf29b85b9e11..ec4640822aa0e5 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -762,7 +762,15 @@ core.multiPackIndex:: information. Defaults to true. core.gvfs:: - Enable the features needed for GVFS. + Enable the features needed for GVFS. This value can be set to true + to indicate all features should be turned on or the bit values listed + below can be used to turn on specific features. ++ +-- + GVFS_SKIP_SHA_ON_INDEX:: + Bit value 1 + Disables the calculation of the sha when writing the index +-- core.sparseCheckout:: Enable "sparse checkout" feature. See linkgit:git-sparse-checkout[1] diff --git a/gvfs.h b/gvfs.h index 01b748ebcecc99..9bf5d00b365d90 100644 --- a/gvfs.h +++ b/gvfs.h @@ -8,6 +8,11 @@ struct repository; * used for GVFS functionality */ +/* + * The list of bits in the core_gvfs setting + */ +#define GVFS_SKIP_SHA_ON_INDEX (1 << 0) + int gvfs_config_is_set(struct repository *r, int mask); #endif /* GVFS_H */ diff --git a/repo-settings.c b/repo-settings.c index 195c24e9c07606..279b18a97e9f5c 100644 --- a/repo-settings.c +++ b/repo-settings.c @@ -5,6 +5,7 @@ #include "midx.h" #include "pack-objects.h" #include "setup.h" +#include "gvfs.h" static void repo_cfg_bool(struct repository *r, const char *key, int *dest, int def) @@ -85,6 +86,13 @@ void prepare_repo_settings(struct repository *r) r->settings.pack_use_bitmap_boundary_traversal); repo_cfg_bool(r, "core.usereplacerefs", &r->settings.read_replace_refs, 1); + /* + * For historical compatibility reasons, enable index.skipHash based + * on a bit in core.gvfs. + */ + if (gvfs_config_is_set(r, GVFS_SKIP_SHA_ON_INDEX)) + r->settings.index_skip_hash = 1; + /* * The GIT_TEST_MULTI_PACK_INDEX variable is special in that * either it *or* the config sets diff --git a/t/meson.build b/t/meson.build index 1fe7585621ef0b..b1bb6190f3fd96 100644 --- a/t/meson.build +++ b/t/meson.build @@ -165,6 +165,7 @@ integration_tests = [ 't1014-read-tree-confusing.sh', 't1015-read-index-unmerged.sh', 't1016-compatObjectFormat.sh', + 't1017-read-tree-skip-sha-on-read.sh', 't1020-subdirectory.sh', 't1022-read-tree-partial-clone.sh', 't1050-large.sh', diff --git a/t/t1017-read-tree-skip-sha-on-read.sh b/t/t1017-read-tree-skip-sha-on-read.sh new file mode 100755 index 00000000000000..5b76a80a0020dc --- /dev/null +++ b/t/t1017-read-tree-skip-sha-on-read.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +test_description='check that read-tree works with core.gvfs config value' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-read-tree.sh + +test_expect_success setup ' + echo one >a && + git add a && + git commit -m initial +' +test_expect_success 'read-tree without core.gvsf' ' + read_tree_u_must_succeed -m -u HEAD +' + +test_expect_success 'read-tree with core.gvfs set to 1' ' + git config core.gvfs 1 && + read_tree_u_must_succeed -m -u HEAD +' + +test_done From 821ea8e617c7f850a470a9e197d516f6fde33b6e Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Tue, 24 Jan 2017 17:54:55 +0100 Subject: [PATCH 051/229] gvfs: add the feature that blobs may be missing Signed-off-by: Kevin Willford --- Documentation/config/core.adoc | 4 ++++ cache-tree.c | 4 +++- commit.c | 9 +++++++-- gvfs.h | 1 + t/t0000-basic.sh | 5 +++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index ec4640822aa0e5..90075213e1a16c 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -770,6 +770,10 @@ core.gvfs:: GVFS_SKIP_SHA_ON_INDEX:: Bit value 1 Disables the calculation of the sha when writing the index + GVFS_MISSING_OK:: + Bit value 4 + Normally git write-tree ensures that the objects referenced by the + directory exist in the object database. This option disables this check. -- core.sparseCheckout:: diff --git a/cache-tree.c b/cache-tree.c index 2aba47060e95d4..60416dc3b973ab 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -4,6 +4,7 @@ #include "git-compat-util.h" #include "gettext.h" #include "hex.h" +#include "gvfs.h" #include "lockfile.h" #include "tree.h" #include "tree-walk.h" @@ -262,7 +263,8 @@ static int update_one(struct cache_tree *it, int flags) { struct strbuf buffer; - int missing_ok = flags & WRITE_TREE_MISSING_OK; + int missing_ok = gvfs_config_is_set(the_repository, GVFS_MISSING_OK) ? + WRITE_TREE_MISSING_OK : (flags & WRITE_TREE_MISSING_OK); int dryrun = flags & WRITE_TREE_DRY_RUN; int repair = flags & WRITE_TREE_REPAIR; int to_invalidate = 0; diff --git a/commit.c b/commit.c index 16d91b2bfcf291..9dba840a9cd279 100644 --- a/commit.c +++ b/commit.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "gvfs.h" #include "tag.h" #include "commit.h" #include "commit-graph.h" @@ -558,13 +559,17 @@ int repo_parse_commit_internal(struct repository *r, .sizep = &size, .contentp = &buffer, }; + int ret; /* * Git does not support partial clones that exclude commits, so set * OBJECT_INFO_SKIP_FETCH_OBJECT to fail fast when an object is missing. */ int flags = OBJECT_INFO_LOOKUP_REPLACE | OBJECT_INFO_SKIP_FETCH_OBJECT | - OBJECT_INFO_DIE_IF_CORRUPT; - int ret; + OBJECT_INFO_DIE_IF_CORRUPT; + + /* But the GVFS Protocol _does_ support missing commits! */ + if (gvfs_config_is_set(r, GVFS_MISSING_OK)) + flags ^= OBJECT_INFO_SKIP_FETCH_OBJECT; if (!item) return -1; diff --git a/gvfs.h b/gvfs.h index 9bf5d00b365d90..88e5b8665754bc 100644 --- a/gvfs.h +++ b/gvfs.h @@ -12,6 +12,7 @@ struct repository; * The list of bits in the core_gvfs setting */ #define GVFS_SKIP_SHA_ON_INDEX (1 << 0) +#define GVFS_MISSING_OK (1 << 2) int gvfs_config_is_set(struct repository *r, int mask); diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh index 2b63e1c86cae30..5ebb98ea7bf1a8 100755 --- a/t/t0000-basic.sh +++ b/t/t0000-basic.sh @@ -1109,6 +1109,11 @@ test_expect_success 'writing this tree with --missing-ok' ' git write-tree --missing-ok ' +test_expect_success 'writing this tree with missing ok config value' ' + git config core.gvfs 4 && + git write-tree +' + ################################################################ test_expect_success 'git read-tree followed by write-tree should be idempotent' ' From 68d9b9f022cb4ea12cb4f9201181fb6566da1c03 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Wed, 18 May 2016 13:40:39 +0000 Subject: [PATCH 052/229] gvfs: prevent files to be deleted outside the sparse checkout Prevent the sparse checkout to delete files that were marked with skip-worktree bit and are not in the sparse-checkout file. This is because everything with the skip-worktree bit turned on is being virtualized and will be removed with the change of HEAD. There was only one failing test when running with these changes that was checking to make sure the worktree narrows on checkout which was expected since we would no longer be narrowing the worktree. Update 2022-04-05: temporarily set 'sparse.expectfilesoutsideofpatterns' in test (until we start disabling the "remove present-despite-SKIP_WORKTREE" behavior with 'core.virtualfilesystem' in a later commit). Signed-off-by: Kevin Willford --- Documentation/config/core.adoc | 9 +++++++++ gvfs.h | 1 + t/t1090-sparse-checkout-scope.sh | 20 ++++++++++++++++++++ unpack-trees.c | 22 ++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 90075213e1a16c..e4a4f4d9419685 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -774,6 +774,15 @@ core.gvfs:: Bit value 4 Normally git write-tree ensures that the objects referenced by the directory exist in the object database. This option disables this check. + GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT:: + Bit value 8 + When marking entries to remove from the index and the working + directory this option will take into account what the + skip-worktree bit was set to so that if the entry has the + skip-worktree bit set it will not be removed from the working + directory. This will allow virtualized working directories to + detect the change to HEAD and use the new commit tree to show + the files that are in the working directory. -- core.sparseCheckout:: diff --git a/gvfs.h b/gvfs.h index 88e5b8665754bc..78ef57d05650b0 100644 --- a/gvfs.h +++ b/gvfs.h @@ -13,6 +13,7 @@ struct repository; */ #define GVFS_SKIP_SHA_ON_INDEX (1 << 0) #define GVFS_MISSING_OK (1 << 2) +#define GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT (1 << 3) int gvfs_config_is_set(struct repository *r, int mask); diff --git a/t/t1090-sparse-checkout-scope.sh b/t/t1090-sparse-checkout-scope.sh index 529844e2862c74..effa20aab7bea7 100755 --- a/t/t1090-sparse-checkout-scope.sh +++ b/t/t1090-sparse-checkout-scope.sh @@ -106,6 +106,26 @@ test_expect_success 'in partial clone, sparse checkout only fetches needed blobs test_cmp expect actual ' +test_expect_success 'checkout does not delete items outside the sparse checkout file' ' + # The "sparse.expectfilesoutsideofpatterns" config will prevent the + # SKIP_WORKTREE flag from being dropped on files present on-disk. + test_config sparse.expectfilesoutsideofpatterns true && + + test_config core.gvfs 8 && + git checkout -b outside && + echo "new file1" >d && + git add --sparse d && + git commit -m "branch initial" && + echo "new file1" >e && + git add --sparse e && + git commit -m "skipped worktree" && + git update-index --skip-worktree e && + echo "/d" >.git/info/sparse-checkout && + git checkout HEAD^ && + test_path_is_file d && + test_path_is_file e +' + test_expect_success MINGW 'no unnecessary opendir() with fscache' ' git clone . fscache-test && ( diff --git a/unpack-trees.c b/unpack-trees.c index 450dbdf7c1bd6c..5c74b5374dc67a 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "advice.h" +#include "gvfs.h" #include "strvec.h" #include "repository.h" #include "parse.h" @@ -2688,6 +2689,27 @@ static int deleted_entry(const struct cache_entry *ce, if (!(old->ce_flags & CE_CONFLICTED) && verify_uptodate(old, o)) return -1; + + /* + * When marking entries to remove from the index and the working + * directory this option will take into account what the + * skip-worktree bit was set to so that if the entry has the + * skip-worktree bit set it will not be removed from the working + * directory. This will allow virtualized working directories to + * detect the change to HEAD and use the new commit tree to show + * the files that are in the working directory. + * + * old is the cache_entry that will have the skip-worktree bit set + * which will need to be preserved when the CE_REMOVE entry is added + */ + if (gvfs_config_is_set(the_repository, GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT) && + old && + old->ce_flags & CE_SKIP_WORKTREE) { + add_entry(o, old, CE_REMOVE, 0); + invalidate_ce_path(old, o); + return 1; + } + add_entry(o, ce, CE_REMOVE, 0); invalidate_ce_path(ce, o); return 1; From 9bd1d2e084b3e6d1a1c315815c9da14fbf71b65e Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Mon, 30 May 2016 10:55:53 -0400 Subject: [PATCH 053/229] gvfs: optionally skip reachability checks/upload pack during fetch While performing a fetch with a virtual file system we know that there will be missing objects and we don't want to download them just because of the reachability of the commits. We also don't want to download a pack file with commits, trees, and blobs since these will be downloaded on demand. This flag will skip the first connectivity check and by returning zero will skip the upload pack. It will also skip the second connectivity check but continue to update the branches to the latest commit ids. Signed-off-by: Kevin Willford --- Documentation/config/core.adoc | 9 +++++++++ connected.c | 19 +++++++++++++++++++ gvfs.h | 1 + t/meson.build | 1 + t/t5584-vfs.sh | 24 ++++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100755 t/t5584-vfs.sh diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index e4a4f4d9419685..ac7b51a7e02641 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -783,6 +783,15 @@ core.gvfs:: directory. This will allow virtualized working directories to detect the change to HEAD and use the new commit tree to show the files that are in the working directory. + GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK:: + Bit value 16 + While performing a fetch with a virtual file system we know + that there will be missing objects and we don't want to download + them just because of the reachability of the commits. We also + don't want to download a pack file with commits, trees, and blobs + since these will be downloaded on demand. This flag will skip the + checks on the reachability of objects during a fetch as well as + the upload pack so that extraneous objects don't get downloaded. -- core.sparseCheckout:: diff --git a/connected.c b/connected.c index 79403108dd8f57..207cb11c666c95 100644 --- a/connected.c +++ b/connected.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "gettext.h" +#include "gvfs.h" #include "hex.h" #include "odb.h" #include "run-command.h" @@ -34,6 +35,24 @@ int check_connected(oid_iterate_fn fn, void *cb_data, struct transport *transport; size_t base_len; + /* + * Running a virtual file system there will be objects that are + * missing locally and we don't want to download a bunch of + * commits, trees, and blobs just to make sure everything is + * reachable locally so this option will skip reachablility + * checks below that use rev-list. This will stop the check + * before uploadpack runs to determine if there is anything to + * fetch. Returning zero for the first check will also prevent the + * uploadpack from happening. It will also skip the check after + * the fetch is finished to make sure all the objects where + * downloaded in the pack file. This will allow the fetch to + * run and get all the latest tip commit ids for all the branches + * in the fetch but not pull down commits, trees, or blobs via + * upload pack. + */ + if (gvfs_config_is_set(the_repository, GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK)) + return 0; + if (!opt) opt = &defaults; transport = opt->transport; diff --git a/gvfs.h b/gvfs.h index 78ef57d05650b0..87938f0b10857f 100644 --- a/gvfs.h +++ b/gvfs.h @@ -14,6 +14,7 @@ struct repository; #define GVFS_SKIP_SHA_ON_INDEX (1 << 0) #define GVFS_MISSING_OK (1 << 2) #define GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT (1 << 3) +#define GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK (1 << 4) int gvfs_config_is_set(struct repository *r, int mask); diff --git a/t/meson.build b/t/meson.build index b1bb6190f3fd96..3b1104914173d3 100644 --- a/t/meson.build +++ b/t/meson.build @@ -700,6 +700,7 @@ integration_tests = [ 't5581-http-curl-verbose.sh', 't5582-fetch-negative-refspec.sh', 't5583-push-branches.sh', + 't5584-vfs.sh', 't5600-clone-fail-cleanup.sh', 't5601-clone.sh', 't5602-clone-remote-exec.sh', diff --git a/t/t5584-vfs.sh b/t/t5584-vfs.sh new file mode 100755 index 00000000000000..8a703cbb640387 --- /dev/null +++ b/t/t5584-vfs.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +test_description='fetch using the flag to skip reachability and upload pack' + +. ./test-lib.sh + + +test_expect_success setup ' + echo inital >a && + git add a && + git commit -m initial && + git clone . one +' + +test_expect_success "fetch test" ' + cd one && + git config core.gvfs 16 && + rm -rf .git/objects/* && + git -C .. cat-file commit HEAD | git hash-object -w --stdin -t commit && + git fetch && + test_must_fail git rev-parse --verify HEAD^{tree} +' + +test_done \ No newline at end of file From e0b06664ca0d91c0edf2a5ee56de84eaf4318a9d Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Wed, 15 Jun 2016 14:59:16 +0000 Subject: [PATCH 054/229] gvfs: ensure all filters and EOL conversions are blocked Ensure all filters and EOL conversions are blocked when running under GVFS so that our projected file sizes will match the actual file size when it is hydrated on the local machine. Signed-off-by: Ben Peart --- Documentation/config/core.adoc | 9 +++++++++ convert.c | 23 +++++++++++++++++++++ gvfs.h | 1 + t/t0021-conversion.sh | 37 ++++++++++++++++++++++++++++++++++ t/t0027-auto-crlf.sh | 12 +++++++++++ 5 files changed, 82 insertions(+) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index ac7b51a7e02641..52133689ebafae 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -792,6 +792,15 @@ core.gvfs:: since these will be downloaded on demand. This flag will skip the checks on the reachability of objects during a fetch as well as the upload pack so that extraneous objects don't get downloaded. + GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS:: + Bit value 64 + With a virtual file system we only know the file size before any + CRLF or smudge/clean filters processing is done on the client. + To prevent file corruption due to truncation or expansion with + garbage at the end, these filters must not run when the file + is first accessed and brought down to the client. Git.exe can't + currently tell the first access vs subsequent accesses so this + flag just blocks them from occurring at all. -- core.sparseCheckout:: diff --git a/convert.c b/convert.c index c7d6a85c226db7..7bf10d3178d752 100644 --- a/convert.c +++ b/convert.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "advice.h" +#include "gvfs.h" #include "config.h" #include "convert.h" #include "copy.h" @@ -563,6 +564,10 @@ static int crlf_to_git(struct index_state *istate, if (!buf) return 1; + if (gvfs_config_is_set(istate && istate->repo ? istate->repo : the_repository, + GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("CRLF conversions not supported when running under GVFS"); + /* only grow if not in place */ if (strbuf_avail(buf) + buf->len < len) strbuf_grow(buf, len - buf->len); @@ -602,6 +607,9 @@ static int crlf_to_worktree(const char *src, size_t len, struct strbuf *buf, if (!will_convert_lf_to_crlf(&stats, crlf_action)) return 0; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("CRLF conversions not supported when running under GVFS"); + /* are we "faking" in place editing ? */ if (src == buf->buf) to_free = strbuf_detach(buf, NULL); @@ -711,6 +719,9 @@ static int apply_single_file_filter(const char *path, const char *src, size_t le struct async async; struct filter_params params; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("Filter \"%s\" not supported when running under GVFS", cmd); + memset(&async, 0, sizeof(async)); async.proc = filter_buffer_or_fd; async.data = ¶ms; @@ -1130,6 +1141,9 @@ static int ident_to_git(const char *src, size_t len, if (!buf) return 1; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("ident conversions not supported when running under GVFS"); + /* only grow if not in place */ if (strbuf_avail(buf) + buf->len < len) strbuf_grow(buf, len - buf->len); @@ -1177,6 +1191,9 @@ static int ident_to_worktree(const char *src, size_t len, if (!cnt) return 0; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("ident conversions not supported when running under GVFS"); + /* are we "faking" in place editing ? */ if (src == buf->buf) to_free = strbuf_detach(buf, NULL); @@ -1629,6 +1646,9 @@ static int lf_to_crlf_filter_fn(struct stream_filter *filter, size_t count, o = 0; struct lf_to_crlf_filter *lf_to_crlf = (struct lf_to_crlf_filter *)filter; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("CRLF conversions not supported when running under GVFS"); + /* * We may be holding onto the CR to see if it is followed by a * LF, in which case we would need to go to the main loop. @@ -1873,6 +1893,9 @@ static int ident_filter_fn(struct stream_filter *filter, struct ident_filter *ident = (struct ident_filter *)filter; static const char head[] = "$Id"; + if (gvfs_config_is_set(the_repository, GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS)) + die("ident conversions not supported when running under GVFS"); + if (!input) { /* drain upon eof */ switch (ident->state) { diff --git a/gvfs.h b/gvfs.h index 87938f0b10857f..13a963facde366 100644 --- a/gvfs.h +++ b/gvfs.h @@ -15,6 +15,7 @@ struct repository; #define GVFS_MISSING_OK (1 << 2) #define GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT (1 << 3) #define GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK (1 << 4) +#define GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS (1 << 6) int gvfs_config_is_set(struct repository *r, int mask); diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index 0c5975336f2104..2b97db87b0f96e 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -334,6 +334,43 @@ test_expect_success "filter: smudge empty file" ' test_cmp expected filtered-empty-in-repo ' +test_expect_success "filter: clean filters blocked when under GVFS" ' + test_config filter.empty-in-repo.clean "cat >/dev/null" && + test_config filter.empty-in-repo.smudge "echo smudged && cat" && + test_config core.gvfs 64 && + + echo dead data walking >empty-in-repo && + test_must_fail git add empty-in-repo +' + +test_expect_success "filter: smudge filters blocked when under GVFS" ' + test_config filter.empty-in-repo.clean "cat >/dev/null" && + test_config filter.empty-in-repo.smudge "echo smudged && cat" && + test_config core.gvfs 64 && + + test_must_fail git checkout +' + +test_expect_success "ident blocked on add when under GVFS" ' + test_config core.gvfs 64 && + test_config core.autocrlf false && + + echo "*.i ident" >.gitattributes && + echo "\$Id\$" > ident.i && + + test_must_fail git add ident.i +' + +test_expect_success "ident blocked when under GVFS" ' + git add ident.i && + + git commit -m "added ident.i" && + test_config core.gvfs 64 && + rm ident.i && + + test_must_fail git checkout -- ident.i +' + test_expect_success 'disable filter with empty override' ' test_config_global filter.disable.smudge false && test_config_global filter.disable.clean false && diff --git a/t/t0027-auto-crlf.sh b/t/t0027-auto-crlf.sh index 49dbf09da77386..8f42f28f56ab43 100755 --- a/t/t0027-auto-crlf.sh +++ b/t/t0027-auto-crlf.sh @@ -343,6 +343,18 @@ checkout_files () { " } +test_expect_success 'crlf conversions blocked when under GVFS' ' + git checkout -b gvfs && + test_commit initial && + rm initial.t && + test_config core.gvfs 64 && + test_config core.autocrlf true && + test_must_fail git read-tree --reset -u HEAD && + + git config core.autocrlf false && + git read-tree --reset -u HEAD +' + # Test control characters # NUL SOH CR EOF==^Z test_expect_success 'ls-files --eol -o Text/Binary' ' From 6d06207d37086b8885e39da853fe21419b504b62 Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Tue, 10 Jan 2017 18:47:14 +0000 Subject: [PATCH 055/229] gvfs: allow "virtualizing" objects The idea is to allow blob objects to be missing from the local repository, and to load them lazily on demand. After discussing this idea on the mailing list, we will rename the feature to "lazy clone" and work more on this. Signed-off-by: Ben Peart Signed-off-by: Johannes Schindelin --- connected.c | 3 +++ environment.c | 1 + environment.h | 1 + gvfs.c | 17 +++++++++++++++-- gvfs.h | 1 + odb.c | 24 ++++++++++++++++++++++++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/connected.c b/connected.c index 207cb11c666c95..e496c68c7cf569 100644 --- a/connected.c +++ b/connected.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "environment.h" #include "gettext.h" #include "gvfs.h" #include "hex.h" @@ -52,6 +53,8 @@ int check_connected(oid_iterate_fn fn, void *cb_data, */ if (gvfs_config_is_set(the_repository, GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK)) return 0; + if (gvfs_virtualize_objects(the_repository)) + return 0; if (!opt) opt = &defaults; diff --git a/environment.c b/environment.c index 316e1e6f69a639..85b68f0be8fa1b 100644 --- a/environment.c +++ b/environment.c @@ -80,6 +80,7 @@ int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ unsigned long pack_size_limit_cfg; +int core_virtualize_objects; int max_allowed_tree_depth = #ifdef _MSC_VER /* diff --git a/environment.h b/environment.h index e61f843fdbb637..aca212b53e8cd9 100644 --- a/environment.h +++ b/environment.h @@ -215,5 +215,6 @@ extern int auto_comment_line_char; extern bool warn_on_auto_comment_char; #endif /* !WITH_BREAKING_CHANGES */ +extern int core_virtualize_objects; # endif /* USE_THE_REPOSITORY_VARIABLE */ #endif /* ENVIRONMENT_H */ diff --git a/gvfs.c b/gvfs.c index 0c43639bcb21de..5e644f2731bc6e 100644 --- a/gvfs.c +++ b/gvfs.c @@ -16,6 +16,8 @@ static int early_core_gvfs_config(const char *var, const char *value, if (!strcmp(var, "core.gvfs")) core_gvfs = git_config_bool_or_int("core.gvfs", value, ctx->kvi, &core_gvfs_is_bool); + if (!strcmp(var, "core.virtualizeobjects")) + core_virtualize_objects = git_config_bool(var, value); return 0; } @@ -27,11 +29,16 @@ static void gvfs_load_config_value(struct repository *r) if (r) { repo_config_get_bool_or_int(r, "core.gvfs", &core_gvfs_is_bool, &core_gvfs); - } else if (startup_info->have_repository == 0) + repo_config_get_bool(r, "core.virtualizeobjects", + &core_virtualize_objects); + } else if (startup_info->have_repository == 0) { read_early_config(the_repository, early_core_gvfs_config, NULL); - else + } else { repo_config_get_bool_or_int(the_repository, "core.gvfs", &core_gvfs_is_bool, &core_gvfs); + repo_config_get_bool(the_repository, "core.virtualizeobjects", + &core_virtualize_objects); + } /* Turn on all bits if a bool was set in the settings */ if (core_gvfs_is_bool && core_gvfs) @@ -46,3 +53,9 @@ int gvfs_config_is_set(struct repository *r, int mask) gvfs_load_config_value(r); return (core_gvfs & mask) == mask; } + +int gvfs_virtualize_objects(struct repository *r) +{ + gvfs_load_config_value(r); + return core_virtualize_objects; +} diff --git a/gvfs.h b/gvfs.h index 13a963facde366..e3995b586cddd5 100644 --- a/gvfs.h +++ b/gvfs.h @@ -18,5 +18,6 @@ struct repository; #define GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS (1 << 6) int gvfs_config_is_set(struct repository *r, int mask); +int gvfs_virtualize_objects(struct repository *r); #endif /* GVFS_H */ diff --git a/odb.c b/odb.c index 00a6e71568b598..a0a7d45933af59 100644 --- a/odb.c +++ b/odb.c @@ -5,7 +5,9 @@ #include "dir.h" #include "environment.h" #include "gettext.h" +#include "gvfs.h" #include "hex.h" +#include "hook.h" #include "khash.h" #include "lockfile.h" #include "loose.h" @@ -23,6 +25,7 @@ #include "strvec.h" #include "submodule.h" #include "trace2.h" +#include "trace.h" #include "write-or-die.h" KHASH_INIT(odb_path_map, const char * /* key: odb_path */, @@ -628,6 +631,20 @@ void disable_obj_read_lock(void) pthread_mutex_destroy(&obj_read_mutex); } +static int run_read_object_hook(struct repository *r, const struct object_id *oid) +{ + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + int ret; + uint64_t start; + + start = getnanotime(); + strvec_push(&opt.args, oid_to_hex(oid)); + ret = run_hooks_opt(r, "read-object", &opt); + trace_performance_since(start, "run_read_object_hook"); + + return ret; +} + int fetch_if_missing = 1; static int register_all_submodule_sources(struct object_database *odb) @@ -657,6 +674,7 @@ static int do_oid_object_info_extended(struct object_database *odb, int rtype; const struct object_id *real = oid; int already_retried = 0; + int tried_hook = 0; if (flags & OBJECT_INFO_LOOKUP_REPLACE) @@ -668,6 +686,7 @@ static int do_oid_object_info_extended(struct object_database *odb, if (!oi) oi = &blank_oi; +retry: co = find_cached_object(odb, real); if (co) { if (oi->typep) @@ -697,6 +716,11 @@ static int do_oid_object_info_extended(struct object_database *odb, odb_reprepare(odb->repo->objects); if (find_pack_entry(odb->repo, real, &e)) break; + if (gvfs_virtualize_objects(odb->repo) && !tried_hook) { + tried_hook = 1; + if (!run_read_object_hook(odb->repo, oid)) + goto retry; + } } /* From 9e9710e18d8ce697b33b5bf51273b00c6dae2404 Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Wed, 15 Mar 2017 18:43:05 +0000 Subject: [PATCH 056/229] Hydrate missing loose objects in check_and_freshen() Hydrate missing loose objects in check_and_freshen() when running virtualized. Add test cases to verify read-object hook works when running virtualized. This hook is called in check_and_freshen() rather than check_and_freshen_local() to make the hook work also with alternates. Helped-by: Kevin Willford Signed-off-by: Ben Peart --- .../technical/read-object-protocol.txt | 102 ++++++++++++++ contrib/long-running-read-object/example.pl | 114 +++++++++++++++ object-file.c | 12 +- odb.c | 132 ++++++++++++++++-- odb.h | 2 + t/meson.build | 1 + t/t0410/read-object | 114 +++++++++++++++ t/t0499-read-object.sh | 30 ++++ 8 files changed, 491 insertions(+), 16 deletions(-) create mode 100644 Documentation/technical/read-object-protocol.txt create mode 100644 contrib/long-running-read-object/example.pl create mode 100755 t/t0410/read-object create mode 100755 t/t0499-read-object.sh diff --git a/Documentation/technical/read-object-protocol.txt b/Documentation/technical/read-object-protocol.txt new file mode 100644 index 00000000000000..a893b46e7c28a9 --- /dev/null +++ b/Documentation/technical/read-object-protocol.txt @@ -0,0 +1,102 @@ +Read Object Process +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The read-object process enables Git to read all missing blobs with a +single process invocation for the entire life of a single Git command. +This is achieved by using a packet format (pkt-line, see technical/ +protocol-common.txt) based protocol over standard input and standard +output as follows. All packets, except for the "*CONTENT" packets and +the "0000" flush packet, are considered text and therefore are +terminated by a LF. + +Git starts the process when it encounters the first missing object that +needs to be retrieved. After the process is started, Git sends a welcome +message ("git-read-object-client"), a list of supported protocol version +numbers, and a flush packet. Git expects to read a welcome response +message ("git-read-object-server"), exactly one protocol version number +from the previously sent list, and a flush packet. All further +communication will be based on the selected version. + +The remaining protocol description below documents "version=1". Please +note that "version=42" in the example below does not exist and is only +there to illustrate how the protocol would look with more than one +version. + +After the version negotiation Git sends a list of all capabilities that +it supports and a flush packet. Git expects to read a list of desired +capabilities, which must be a subset of the supported capabilities list, +and a flush packet as response: +------------------------ +packet: git> git-read-object-client +packet: git> version=1 +packet: git> version=42 +packet: git> 0000 +packet: git< git-read-object-server +packet: git< version=1 +packet: git< 0000 +packet: git> capability=get +packet: git> capability=have +packet: git> capability=put +packet: git> capability=not-yet-invented +packet: git> 0000 +packet: git< capability=get +packet: git< 0000 +------------------------ +The only supported capability in version 1 is "get". + +Afterwards Git sends a list of "key=value" pairs terminated with a flush +packet. The list will contain at least the command (based on the +supported capabilities) and the sha1 of the object to retrieve. Please +note, that the process must not send any response before it received the +final flush packet. + +When the process receives the "get" command, it should make the requested +object available in the git object store and then return success. Git will +then check the object store again and this time find it and proceed. +------------------------ +packet: git> command=get +packet: git> sha1=0a214a649e1b3d5011e14a3dc227753f2bd2be05 +packet: git> 0000 +------------------------ + +The process is expected to respond with a list of "key=value" pairs +terminated with a flush packet. If the process does not experience +problems then the list must contain a "success" status. +------------------------ +packet: git< status=success +packet: git< 0000 +------------------------ + +In case the process cannot or does not want to process the content, it +is expected to respond with an "error" status. +------------------------ +packet: git< status=error +packet: git< 0000 +------------------------ + +In case the process cannot or does not want to process the content as +well as any future content for the lifetime of the Git process, then it +is expected to respond with an "abort" status at any point in the +protocol. +------------------------ +packet: git< status=abort +packet: git< 0000 +------------------------ + +Git neither stops nor restarts the process in case the "error"/"abort" +status is set. + +If the process dies during the communication or does not adhere to the +protocol then Git will stop the process and restart it with the next +object that needs to be processed. + +After the read-object process has processed an object it is expected to +wait for the next "key=value" list containing a command. Git will close +the command pipe on exit. The process is expected to detect EOF and exit +gracefully on its own. Git will wait until the process has stopped. + +A long running read-object process demo implementation can be found in +`contrib/long-running-read-object/example.pl` located in the Git core +repository. If you develop your own long running process then the +`GIT_TRACE_PACKET` environment variables can be very helpful for +debugging (see linkgit:git[1]). diff --git a/contrib/long-running-read-object/example.pl b/contrib/long-running-read-object/example.pl new file mode 100644 index 00000000000000..b8f37f836a813c --- /dev/null +++ b/contrib/long-running-read-object/example.pl @@ -0,0 +1,114 @@ +#!/usr/bin/perl +# +# Example implementation for the Git read-object protocol version 1 +# See Documentation/technical/read-object-protocol.txt +# +# Allows you to test the ability for blobs to be pulled from a host git repo +# "on demand." Called when git needs a blob it couldn't find locally due to +# a lazy clone that only cloned the commits and trees. +# +# A lazy clone can be simulated via the following commands from the host repo +# you wish to create a lazy clone of: +# +# cd /host_repo +# git rev-parse HEAD +# git init /guest_repo +# git cat-file --batch-check --batch-all-objects | grep -v 'blob' | +# cut -d' ' -f1 | git pack-objects /guest_repo/.git/objects/pack/noblobs +# cd /guest_repo +# git config core.virtualizeobjects true +# git reset --hard +# +# Please note, this sample is a minimal skeleton. No proper error handling +# was implemented. +# + +use strict; +use warnings; + +# +# Point $DIR to the folder where your host git repo is located so we can pull +# missing objects from it +# +my $DIR = "/host_repo/.git/"; + +sub packet_bin_read { + my $buffer; + my $bytes_read = read STDIN, $buffer, 4; + if ( $bytes_read == 0 ) { + + # EOF - Git stopped talking to us! + exit(); + } + elsif ( $bytes_read != 4 ) { + die "invalid packet: '$buffer'"; + } + my $pkt_size = hex($buffer); + if ( $pkt_size == 0 ) { + return ( 1, "" ); + } + elsif ( $pkt_size > 4 ) { + my $content_size = $pkt_size - 4; + $bytes_read = read STDIN, $buffer, $content_size; + if ( $bytes_read != $content_size ) { + die "invalid packet ($content_size bytes expected; $bytes_read bytes read)"; + } + return ( 0, $buffer ); + } + else { + die "invalid packet size: $pkt_size"; + } +} + +sub packet_txt_read { + my ( $res, $buf ) = packet_bin_read(); + unless ( $buf =~ s/\n$// ) { + die "A non-binary line MUST be terminated by an LF."; + } + return ( $res, $buf ); +} + +sub packet_bin_write { + my $buf = shift; + print STDOUT sprintf( "%04x", length($buf) + 4 ); + print STDOUT $buf; + STDOUT->flush(); +} + +sub packet_txt_write { + packet_bin_write( $_[0] . "\n" ); +} + +sub packet_flush { + print STDOUT sprintf( "%04x", 0 ); + STDOUT->flush(); +} + +( packet_txt_read() eq ( 0, "git-read-object-client" ) ) || die "bad initialize"; +( packet_txt_read() eq ( 0, "version=1" ) ) || die "bad version"; +( packet_bin_read() eq ( 1, "" ) ) || die "bad version end"; + +packet_txt_write("git-read-object-server"); +packet_txt_write("version=1"); +packet_flush(); + +( packet_txt_read() eq ( 0, "capability=get" ) ) || die "bad capability"; +( packet_bin_read() eq ( 1, "" ) ) || die "bad capability end"; + +packet_txt_write("capability=get"); +packet_flush(); + +while (1) { + my ($command) = packet_txt_read() =~ /^command=([^=]+)$/; + + if ( $command eq "get" ) { + my ($sha1) = packet_txt_read() =~ /^sha1=([0-9a-f]{40})$/; + packet_bin_read(); + + system ('git --git-dir="' . $DIR . '" cat-file blob ' . $sha1 . ' | git -c core.virtualizeobjects=false hash-object -w --stdin >/dev/null 2>&1'); + packet_txt_write(($?) ? "status=error" : "status=success"); + packet_flush(); + } else { + die "bad command '$command'"; + } +} diff --git a/object-file.c b/object-file.c index c5b74764f10393..8f134e15ec9c6d 100644 --- a/object-file.c +++ b/object-file.c @@ -15,6 +15,7 @@ #include "environment.h" #include "fsck.h" #include "gettext.h" +#include "gvfs.h" #include "hex.h" #include "loose.h" #include "object-file-convert.h" @@ -95,8 +96,17 @@ static int check_and_freshen_source(struct odb_source *source, int freshen) { static struct strbuf path = STRBUF_INIT; + int ret, tried_hook = 0; + odb_loose_path(source, &path, oid); - return check_and_freshen_file(path.buf, freshen); +retry: + ret = check_and_freshen_file(path.buf, freshen); + if (!ret && gvfs_virtualize_objects(source->odb->repo) && !tried_hook) { + tried_hook = 1; + if (!read_object_process(source->odb->repo, oid)) + goto retry; + } + return ret; } int has_loose_object(struct odb_source *source, diff --git a/odb.c b/odb.c index a0a7d45933af59..e8d67d30ec4a95 100644 --- a/odb.c +++ b/odb.c @@ -16,13 +16,16 @@ #include "odb.h" #include "packfile.h" #include "path.h" +#include "pkt-line.h" #include "promisor-remote.h" #include "quote.h" #include "replace-object.h" #include "run-command.h" #include "setup.h" +#include "sigchain.h" #include "strbuf.h" #include "strvec.h" +#include "sub-process.h" #include "submodule.h" #include "trace2.h" #include "trace.h" @@ -610,6 +613,119 @@ int odb_has_alternates(struct object_database *odb) return !!odb->sources->next; } +#define CAP_GET (1u<<0) + +static int subprocess_map_initialized; +static struct hashmap subprocess_map; + +struct read_object_process { + struct subprocess_entry subprocess; + unsigned int supported_capabilities; +}; + +static int start_read_object_fn(struct subprocess_entry *subprocess) +{ + struct read_object_process *entry = (struct read_object_process *)subprocess; + static int versions[] = {1, 0}; + static struct subprocess_capability capabilities[] = { + { "get", CAP_GET }, + { NULL, 0 } + }; + + return subprocess_handshake(subprocess, "git-read-object", versions, + NULL, capabilities, + &entry->supported_capabilities); +} + +int read_object_process(struct repository *r, const struct object_id *oid) +{ + int err; + struct read_object_process *entry; + struct child_process *process; + struct strbuf status = STRBUF_INIT; + const char *cmd = find_hook(r, "read-object"); + uint64_t start; + + if (!cmd) + die(_("could not find the `read-object` hook")); + + start = getnanotime(); + + if (!subprocess_map_initialized) { + subprocess_map_initialized = 1; + hashmap_init(&subprocess_map, (hashmap_cmp_fn)cmd2process_cmp, + NULL, 0); + entry = NULL; + } else { + entry = (struct read_object_process *) subprocess_find_entry(&subprocess_map, cmd); + } + + if (!entry) { + entry = xmalloc(sizeof(*entry)); + entry->supported_capabilities = 0; + + if (subprocess_start(&subprocess_map, &entry->subprocess, cmd, + start_read_object_fn)) { + free(entry); + return -1; + } + } + process = &entry->subprocess.process; + + if (!(CAP_GET & entry->supported_capabilities)) + return -1; + + sigchain_push(SIGPIPE, SIG_IGN); + + err = packet_write_fmt_gently(process->in, "command=get\n"); + if (err) + goto done; + + err = packet_write_fmt_gently(process->in, "sha1=%s\n", oid_to_hex(oid)); + if (err) + goto done; + + err = packet_flush_gently(process->in); + if (err) + goto done; + + err = subprocess_read_status(process->out, &status); + err = err ? err : strcmp(status.buf, "success"); + +done: + sigchain_pop(SIGPIPE); + + if (err || errno == EPIPE) { + err = err ? err : errno; + if (!strcmp(status.buf, "error")) { + /* The process signaled a problem with the file. */ + } + else if (!strcmp(status.buf, "abort")) { + /* + * The process signaled a permanent problem. Don't try to read + * objects with the same command for the lifetime of the current + * Git process. + */ + entry->supported_capabilities &= ~CAP_GET; + } + else { + /* + * Something went wrong with the read-object process. + * Force shutdown and restart if needed. + */ + error("external process '%s' failed", cmd); + subprocess_stop(&subprocess_map, + (struct subprocess_entry *)entry); + free(entry); + } + } + + trace_performance_since(start, "read_object_process"); + + strbuf_release(&status); + return err; +} + int obj_read_use_lock = 0; pthread_mutex_t obj_read_mutex; @@ -631,20 +747,6 @@ void disable_obj_read_lock(void) pthread_mutex_destroy(&obj_read_mutex); } -static int run_read_object_hook(struct repository *r, const struct object_id *oid) -{ - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; - int ret; - uint64_t start; - - start = getnanotime(); - strvec_push(&opt.args, oid_to_hex(oid)); - ret = run_hooks_opt(r, "read-object", &opt); - trace_performance_since(start, "run_read_object_hook"); - - return ret; -} - int fetch_if_missing = 1; static int register_all_submodule_sources(struct object_database *odb) @@ -718,7 +820,7 @@ static int do_oid_object_info_extended(struct object_database *odb, break; if (gvfs_virtualize_objects(odb->repo) && !tried_hook) { tried_hook = 1; - if (!run_read_object_hook(odb->repo, oid)) + if (!read_object_process(odb->repo, oid)) goto retry; } } diff --git a/odb.h b/odb.h index e6602dd90c833c..c9f5a26fb9dc5d 100644 --- a/odb.h +++ b/odb.h @@ -489,4 +489,6 @@ static inline int odb_write_object(struct object_database *odb, return odb_write_object_ext(odb, buf, len, type, oid, NULL, 0); } +int read_object_process(struct repository *r, const struct object_id *oid); + #endif /* ODB_H */ diff --git a/t/meson.build b/t/meson.build index 3b1104914173d3..2b2d85124ac3f7 100644 --- a/t/meson.build +++ b/t/meson.build @@ -139,6 +139,7 @@ integration_tests = [ 't0410-partial-clone.sh', 't0411-clone-from-partial.sh', 't0450-txt-doc-vs-help.sh', + 't0499-read-object.sh', 't0500-progress-display.sh', 't0600-reffiles-backend.sh', 't0601-reffiles-pack-refs.sh', diff --git a/t/t0410/read-object b/t/t0410/read-object new file mode 100755 index 00000000000000..2b8feacc78577f --- /dev/null +++ b/t/t0410/read-object @@ -0,0 +1,114 @@ +#!/usr/bin/perl +# +# Example implementation for the Git read-object protocol version 1 +# See Documentation/technical/read-object-protocol.txt +# +# Allows you to test the ability for blobs to be pulled from a host git repo +# "on demand." Called when git needs a blob it couldn't find locally due to +# a lazy clone that only cloned the commits and trees. +# +# A lazy clone can be simulated via the following commands from the host repo +# you wish to create a lazy clone of: +# +# cd /host_repo +# git rev-parse HEAD +# git init /guest_repo +# git cat-file --batch-check --batch-all-objects | grep -v 'blob' | +# cut -d' ' -f1 | git pack-objects /guest_repo/.git/objects/pack/noblobs +# cd /guest_repo +# git config core.virtualizeobjects true +# git reset --hard +# +# Please note, this sample is a minimal skeleton. No proper error handling +# was implemented. +# + +use strict; +use warnings; + +# +# Point $DIR to the folder where your host git repo is located so we can pull +# missing objects from it +# +my $DIR = "../.git/"; + +sub packet_bin_read { + my $buffer; + my $bytes_read = read STDIN, $buffer, 4; + if ( $bytes_read == 0 ) { + + # EOF - Git stopped talking to us! + exit(); + } + elsif ( $bytes_read != 4 ) { + die "invalid packet: '$buffer'"; + } + my $pkt_size = hex($buffer); + if ( $pkt_size == 0 ) { + return ( 1, "" ); + } + elsif ( $pkt_size > 4 ) { + my $content_size = $pkt_size - 4; + $bytes_read = read STDIN, $buffer, $content_size; + if ( $bytes_read != $content_size ) { + die "invalid packet ($content_size bytes expected; $bytes_read bytes read)"; + } + return ( 0, $buffer ); + } + else { + die "invalid packet size: $pkt_size"; + } +} + +sub packet_txt_read { + my ( $res, $buf ) = packet_bin_read(); + unless ( $buf =~ s/\n$// ) { + die "A non-binary line MUST be terminated by an LF."; + } + return ( $res, $buf ); +} + +sub packet_bin_write { + my $buf = shift; + print STDOUT sprintf( "%04x", length($buf) + 4 ); + print STDOUT $buf; + STDOUT->flush(); +} + +sub packet_txt_write { + packet_bin_write( $_[0] . "\n" ); +} + +sub packet_flush { + print STDOUT sprintf( "%04x", 0 ); + STDOUT->flush(); +} + +( packet_txt_read() eq ( 0, "git-read-object-client" ) ) || die "bad initialize"; +( packet_txt_read() eq ( 0, "version=1" ) ) || die "bad version"; +( packet_bin_read() eq ( 1, "" ) ) || die "bad version end"; + +packet_txt_write("git-read-object-server"); +packet_txt_write("version=1"); +packet_flush(); + +( packet_txt_read() eq ( 0, "capability=get" ) ) || die "bad capability"; +( packet_bin_read() eq ( 1, "" ) ) || die "bad capability end"; + +packet_txt_write("capability=get"); +packet_flush(); + +while (1) { + my ($command) = packet_txt_read() =~ /^command=([^=]+)$/; + + if ( $command eq "get" ) { + my ($sha1) = packet_txt_read() =~ /^sha1=([0-9a-f]{40,64})$/; + packet_bin_read(); + + system ('git --git-dir="' . $DIR . '" cat-file blob ' . $sha1 . ' | git -c core.virtualizeobjects=false hash-object -w --stdin >/dev/null 2>&1'); + packet_txt_write(($?) ? "status=error" : "status=success"); + packet_flush(); + } else { + die "bad command '$command'"; + } +} diff --git a/t/t0499-read-object.sh b/t/t0499-read-object.sh new file mode 100755 index 00000000000000..2e208bdb46add5 --- /dev/null +++ b/t/t0499-read-object.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +test_description='tests for long running read-object process' + +. ./test-lib.sh + +test_expect_success 'setup host repo with a root commit' ' + test_commit zero && + hash1=$(git ls-tree HEAD | grep zero.t | cut -f1 | cut -d\ -f3) +' + +test_expect_success 'blobs can be retrieved from the host repo' ' + git init guest-repo && + (cd guest-repo && + mkdir -p .git/hooks && + sed "1s|/usr/bin/perl|$PERL_PATH|" \ + <$TEST_DIRECTORY/t0410/read-object \ + >.git/hooks/read-object && + chmod +x .git/hooks/read-object && + git config core.virtualizeobjects true && + git cat-file blob "$hash1") +' + +test_expect_success 'invalid blobs generate errors' ' + (cd guest-repo && + test_must_fail git cat-file blob "invalid") +' + + +test_done From 2381f6ac470028e07d29c313aa3bace670a32c7a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Sep 2017 11:32:43 +0200 Subject: [PATCH 057/229] sha1_file: when writing objects, skip the read_object_hook If we are going to write an object there is no use in calling the read object hook to get an object from a potentially remote source. We would rather just write out the object and avoid the potential round trip for an object that doesn't exist. This change adds a flag to the check_and_freshen() and freshen_loose_object() functions' signatures so that the hook is bypassed when the functions are called before writing loose objects. The check for a local object is still performed so we don't overwrite something that has already been written to one of the objects directories. Based on a patch by Kevin Willford. Signed-off-by: Johannes Schindelin --- object-file.c | 16 +++++++++------- t/t0410/read-object | 4 ++++ t/t0499-read-object.sh | 7 +++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/object-file.c b/object-file.c index 8f134e15ec9c6d..6723da7ce12cd8 100644 --- a/object-file.c +++ b/object-file.c @@ -93,7 +93,7 @@ int check_and_freshen_file(const char *fn, int freshen) static int check_and_freshen_source(struct odb_source *source, const struct object_id *oid, - int freshen) + int freshen, int skip_virtualized_objects) { static struct strbuf path = STRBUF_INIT; int ret, tried_hook = 0; @@ -101,7 +101,8 @@ static int check_and_freshen_source(struct odb_source *source, odb_loose_path(source, &path, oid); retry: ret = check_and_freshen_file(path.buf, freshen); - if (!ret && gvfs_virtualize_objects(source->odb->repo) && !tried_hook) { + if (!ret && gvfs_virtualize_objects(source->odb->repo) && + !skip_virtualized_objects && !tried_hook) { tried_hook = 1; if (!read_object_process(source->odb->repo, oid)) goto retry; @@ -112,7 +113,7 @@ static int check_and_freshen_source(struct odb_source *source, int has_loose_object(struct odb_source *source, const struct object_id *oid) { - return check_and_freshen_source(source, oid, 0); + return check_and_freshen_source(source, oid, 0, 0); } int format_object_header(char *str, size_t size, enum object_type type, @@ -997,11 +998,12 @@ static int write_loose_object(struct odb_source *source, } static int freshen_loose_object(struct object_database *odb, - const struct object_id *oid) + const struct object_id *oid, + int skip_virtualized_objects) { odb_prepare_alternates(odb); for (struct odb_source *source = odb->sources; source; source = source->next) - if (check_and_freshen_source(source, oid, 1)) + if (check_and_freshen_source(source, oid, 1, skip_virtualized_objects)) return 1; return 0; } @@ -1102,7 +1104,7 @@ int stream_loose_object(struct odb_source *source, close_loose_object(source, fd, tmp_file.buf); if (freshen_packed_object(source->odb, oid) || - freshen_loose_object(source->odb, oid)) { + freshen_loose_object(source->odb, oid, 1)) { unlink_or_warn(tmp_file.buf); goto cleanup; } @@ -1166,7 +1168,7 @@ int write_object_file(struct odb_source *source, */ write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); if (freshen_packed_object(source->odb, oid) || - freshen_loose_object(source->odb, oid)) + freshen_loose_object(source->odb, oid, 1)) return 0; if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags)) return -1; diff --git a/t/t0410/read-object b/t/t0410/read-object index 2b8feacc78577f..02c799837f4057 100755 --- a/t/t0410/read-object +++ b/t/t0410/read-object @@ -108,6 +108,10 @@ while (1) { system ('git --git-dir="' . $DIR . '" cat-file blob ' . $sha1 . ' | git -c core.virtualizeobjects=false hash-object -w --stdin >/dev/null 2>&1'); packet_txt_write(($?) ? "status=error" : "status=success"); packet_flush(); + + open my $log, '>>.git/read-object-hook.log'; + print $log "Read object $sha1, exit code $?\n"; + close $log; } else { die "bad command '$command'"; } diff --git a/t/t0499-read-object.sh b/t/t0499-read-object.sh index 2e208bdb46add5..0cee1963cf091e 100755 --- a/t/t0499-read-object.sh +++ b/t/t0499-read-object.sh @@ -26,5 +26,12 @@ test_expect_success 'invalid blobs generate errors' ' test_must_fail git cat-file blob "invalid") ' +test_expect_success 'read-object-hook is bypassed when writing objects' ' + (cd guest-repo && + echo hello >hello.txt && + git add hello.txt && + hash="$(git rev-parse --verify :hello.txt)" && + ! grep "$hash" .git/read-object-hook.log) +' test_done From 38b92fdf56b5a47022ed2697e25d28f97338c066 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 22:48:24 +0200 Subject: [PATCH 058/229] git_config_set_multivar_in_file_gently(): add a lock timeout In particular when multiple processes want to write to the config simultaneously, it would come in handy to not fail immediately when another process locked the config, but to gently try again. This will help with Scalar's functional test suite which wants to register multiple repositories for maintenance semi-simultaneously. As not all code paths calling this function read the config (e.g. `git config`), we have to read the config setting via `git_config_get_ulong()`. Signed-off-by: Johannes Schindelin --- Documentation/config/core.adoc | 9 +++++++++ config.c | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 34c44d2c8f8725..90be5e50026600 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -795,3 +795,12 @@ core.WSLCompat:: The default value is false. When set to true, Git will set the mode bits of the file in the way of wsl, so that the executable flag of files can be set or read correctly. + +core.configWriteLockTimeoutMS:: + When processes try to write to the config concurrently, it is likely + that one process "wins" and the other process(es) fail to lock the + config file. By configuring a timeout larger than zero, Git can be + told to try to lock the config again a couple times within the + specified timeout. If the timeout is configure to zero (which is the + default), Git will fail immediately when the config is already + locked. diff --git a/config.c b/config.c index 71b136bf7f6ec0..881c9616e17b8d 100644 --- a/config.c +++ b/config.c @@ -2953,6 +2953,7 @@ int repo_config_set_multivar_in_file_gently(struct repository *r, const char *comment, unsigned flags) { + static unsigned long timeout_ms = ULONG_MAX; int fd = -1, in_fd = -1; int ret; struct lock_file lock = LOCK_INIT; @@ -2981,11 +2982,16 @@ int repo_config_set_multivar_in_file_gently(struct repository *r, if (!config_filename) config_filename = filename_buf = repo_git_path(r, "config"); + if ((long)timeout_ms < 0 && + repo_config_get_ulong(r, "core.configWriteLockTimeoutMS", &timeout_ms)) + timeout_ms = 0; + /* * The lock serves a purpose in addition to locking: the new * contents of .git/config will be written into it. */ - fd = hold_lock_file_for_update(&lock, config_filename, 0); + fd = hold_lock_file_for_update_timeout(&lock, config_filename, 0, + timeout_ms); if (fd < 0) { error_errno(_("could not lock config file %s"), config_filename); ret = CONFIG_NO_LOCK; From 5b689740165c86c7d3fb281c7ba0dc9cbf08503f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 May 2021 23:22:56 +0200 Subject: [PATCH 059/229] scalar: set the config write-lock timeout to 150ms By default, Git fails immediately when locking a config file for writing fails due to an existing lock. With this change, Scalar-registered repositories will fall back to trying a couple times within a 150ms timeout. Signed-off-by: Johannes Schindelin --- scalar.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scalar.c b/scalar.c index f7543116272b77..8308cbfd7dc487 100644 --- a/scalar.c +++ b/scalar.c @@ -172,6 +172,7 @@ static int set_recommended_config(int reconfigure) { "core.safeCRLF", "false" }, { "fetch.showForcedUpdates", "false" }, { "pack.usePathWalk", "true" }, + { "core.configWriteLockTimeoutMS", "150" }, { NULL, NULL }, }; int i; @@ -219,6 +220,11 @@ static int set_recommended_config(int reconfigure) */ static int toggle_maintenance(int enable) { + unsigned long ul; + + if (repo_config_get_ulong(the_repository, "core.configWriteLockTimeoutMS", &ul)) + git_config_push_parameter("core.configWriteLockTimeoutMS=150"); + return run_git("maintenance", enable ? "start" : "unregister", enable ? NULL : "--force", @@ -228,10 +234,14 @@ static int toggle_maintenance(int enable) static int add_or_remove_enlistment(int add) { int res; + unsigned long ul; if (!the_repository->worktree) die(_("Scalar enlistments require a worktree")); + if (repo_config_get_ulong(the_repository, "core.configWriteLockTimeoutMS", &ul)) + git_config_push_parameter("core.configWriteLockTimeoutMS=150"); + res = run_git("config", "--global", "--get", "--fixed-value", "scalar.repo", the_repository->worktree, NULL); From 05640c1af0d2373b0e10312ace386aff39aaf331 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 16 Jun 2021 10:01:37 -0400 Subject: [PATCH 060/229] scalar: add docs from microsoft/scalar These docs have been altered to fit the version implemented in C within microsoft/git. This means in particular that the advanced.md file no longer applied at all. Some other areas were removed or significantly edited. Signed-off-by: Derrick Stolee --- contrib/scalar/docs/faq.md | 51 ++++++++++++++ contrib/scalar/docs/getting-started.md | 93 ++++++++++++++++++++++++++ contrib/scalar/docs/index.md | 50 ++++++++++++++ contrib/scalar/docs/philosophy.md | 66 ++++++++++++++++++ contrib/scalar/docs/troubleshooting.md | 20 ++++++ 5 files changed, 280 insertions(+) create mode 100644 contrib/scalar/docs/faq.md create mode 100644 contrib/scalar/docs/getting-started.md create mode 100644 contrib/scalar/docs/index.md create mode 100644 contrib/scalar/docs/philosophy.md create mode 100644 contrib/scalar/docs/troubleshooting.md diff --git a/contrib/scalar/docs/faq.md b/contrib/scalar/docs/faq.md new file mode 100644 index 00000000000000..a14f78a996d5d5 --- /dev/null +++ b/contrib/scalar/docs/faq.md @@ -0,0 +1,51 @@ +Frequently Asked Questions +========================== + +Using Scalar +------------ + +### I don't want a sparse clone, I want every file after I clone! + +Run `scalar clone --full-clone ` to initialize your repo to include +every file. You can switch to a sparse-checkout later by running +`git sparse-checkout init --cone`. + +### I already cloned without `--full-clone`. How do I get everything? + +Run `git sparse-checkout disable`. + +Scalar Design Decisions +----------------------- + +There may be many design decisions within Scalar that are confusing at first +glance. Some of them may cause friction when you use Scalar with your existing +repos and existing habits. + +> Scalar has the most benefit when users design repositories +> with efficient patterns. + +For example: Scalar uses the sparse-checkout feature to limit the size of the +working directory within a large monorepo. It is designed to work efficiently +with monorepos that are highly componentized, allowing most developers to +need many fewer files in their daily work. + +### Why does `scalar clone` create a `/src` folder? + +Scalar uses a file system watcher to keep track of changes under this `src` folder. +Any activity in this folder is assumed to be important to Git operations. By +creating the `src` folder, we are making it easy for your build system to +create output folders outside the `src` directory. We commonly see systems +create folders for build outputs and package downloads. Scalar itself creates +these folders during its builds. + +Your build system may create build artifacts such as `.obj` or `.lib` files +next to your source code. These are commonly "hidden" from Git using +`.gitignore` files. Having such artifacts in your source tree creates +additional work for Git because it needs to look at these files and match them +against the `.gitignore` patterns. + +By following the `src` pattern Scalar tries to establish and placing your build +intermediates and outputs parallel with the `src` folder and not inside it, +you can help optimize Git command performance for developers in the repository +by limiting the number of files Git needs to consider for many common +operations. diff --git a/contrib/scalar/docs/getting-started.md b/contrib/scalar/docs/getting-started.md new file mode 100644 index 00000000000000..ef7ea07b0f948c --- /dev/null +++ b/contrib/scalar/docs/getting-started.md @@ -0,0 +1,93 @@ +Getting Started +=============== + +Registering existing Git repos +------------------------------ + +To add a repository to the list of registered repos, run `scalar register []`. +If `` is not provided, then the "current repository" is discovered from +the working directory by scanning the parent paths for a path containing a `.git` +folder, possibly inside a `src` folder. + +To see which repositories are currently tracked by the service, run +`scalar list`. + +Run `scalar unregister []` to remove the repo from this list. + +Creating a new Scalar clone +--------------------------------------------------- + +The `clone` verb creates a local enlistment of a remote repository using the +partial clone feature available e.g. on GitHub. + + +``` +scalar clone [options] [] +``` + +Create a local copy of the repository at ``. If specified, create the `` +directory and place the repository there. Otherwise, the last section of the `` +will be used for ``. + +At the end, the repo is located at `/src`. By default, the sparse-checkout +feature is enabled and the only files present are those in the root of your +Git repository. Use `git sparse-checkout set` to expand the set of directories +you want to see, or `git sparse-checkout disable` to expand to all files. You +can explore the subdirectories outside your sparse-checkout specification using +`git ls-tree HEAD`. + +### Sparse Repo Mode + +By default, Scalar reduces your working directory to only the files at the +root of the repository. You need to add the folders you care about to build up +to your working set. + +* `scalar clone ` + * Please choose the **Clone with HTTPS** option in the `Clone Repository` dialog in Azure Repos, not **Clone with SSH**. +* `cd \src` +* At this point, your `src` directory only contains files that appear in your root + tree. No folders are populated. +* Set the directory list for your sparse-checkout using: + 1. `git sparse-checkout set ...` + 2. `git sparse-checkout set --stdin < dir-list.txt` +* Run git commands as you normally would. +* To fully populate your working directory, run `git sparse-checkout disable`. + +If instead you want to start with all files on-disk, you can clone with the +`--full-clone` option. To enable sparse-checkout after the fact, run +`git sparse-checkout init --cone`. This will initialize your sparse-checkout +patterns to only match the files at root. + +If you are unfamiliar with what directories are available in the repository, +then you can run `git ls-tree -d --name-only HEAD` to discover the directories +at root, or `git ls-tree -d --name-only HEAD ` to discover the directories +in ``. + +### Options + +These options allow a user to customize their initial enlistment. + +* `--full-clone`: If specified, do not initialize the sparse-checkout feature. + All files will be present in your `src` directory. This uses a Git partial + clone: blobs are downloaded on demand. + +* `--branch=`: Specify the branch to checkout after clone. + +### Advanced Options + +The options below are not intended for use by a typical user. These are +usually used by build machines to create a temporary enlistment that +operates on a single commit. + +* `--single-branch`: Use this option to only download metadata for the branch + that will be checked out. This is helpful for build machines that target + a remote with many branches. Any `git fetch` commands after the clone will + still ask for all branches. + +Removing a Scalar Clone +----------------------- + +Since the `scalar clone` command sets up a file-system watcher (when available), +that watcher could prevent deleting the enlistment. Run `scalar delete ` +from outside of your enlistment to unregister the enlistment from the filesystem +watcher and delete the enlistment at ``. diff --git a/contrib/scalar/docs/index.md b/contrib/scalar/docs/index.md new file mode 100644 index 00000000000000..f9f5ab06e09253 --- /dev/null +++ b/contrib/scalar/docs/index.md @@ -0,0 +1,50 @@ +Scalar: Enabling Git at Scale +============================= + +Scalar is a tool that helps Git scale to some of the largest Git repositories. +It achieves this by enabling some advanced Git features, such as: + +* *Partial clone:* reduces time to get a working repository by not + downloading all Git objects right away. + +* *Background prefetch:* downloads Git object data from all remotes every + hour, reducing the amount of time for foreground `git fetch` calls. + +* *Sparse-checkout:* limits the size of your working directory. + +* *File system monitor:* tracks the recently modified files and eliminates + the need for Git to scan the entire worktree. + +* *Commit-graph:* accelerates commit walks and reachability calculations, + speeding up commands like `git log`. + +* *Multi-pack-index:* enables fast object lookups across many pack-files. + +* *Incremental repack:* Repacks the packed Git data into fewer pack-file + without disrupting concurrent commands by using the multi-pack-index. + +By running `scalar register` in any Git repo, Scalar will automatically enable +these features for that repo (except partial clone) and start running suggested +maintenance in the background using +[the `git maintenance` feature](https://git-scm.com/docs/git-maintenance). + +Repos cloned with the `scalar clone` command use partial clone to significantly +reduce the amount of data required to get started using a repository. By +delaying all blob downloads until they are required, Scalar allows you to work +with very large repositories quickly. + +Documentation +------------- + +* [Getting Started](getting-started.md): Get started with Scalar. + Includes `scalar register`, `scalar unregister`, `scalar clone`, and + `scalar delete`. + +* [Troubleshooting](troubleshooting.md): + Collect diagnostic information or update custom settings. Includes + `scalar diagnose`. + +* [The Philosophy of Scalar](philosophy.md): Why does Scalar work the way + it does, and how do we make decisions about its future? + +* [Frequently Asked Questions](faq.md) diff --git a/contrib/scalar/docs/philosophy.md b/contrib/scalar/docs/philosophy.md new file mode 100644 index 00000000000000..51486a75e41f0d --- /dev/null +++ b/contrib/scalar/docs/philosophy.md @@ -0,0 +1,66 @@ +The Philosophy of Scalar +======================== + +The team building Scalar has **opinions** about Git performance. Scalar +takes out the guesswork by automatically configuring your Git repositories +to take advantage of the latest and greatest features. It is difficult to +say that these are the absolute best settings for every repository, but +these settings do work for some of the largest repositories in the world. + +Scalar intends to do very little more than the standard Git client. We +actively implement new features into Git instead of Scalar, then update +Scalar only to configure those new settings. In particular, we ported +features like background maintenance to Git to make Scalar simpler and +make Git more powerful. + +Services such as GitHub support partial clone , a standard adopted by the Git +project to download only part of the Git objects when cloning, and fetching +further objects on demand. If your hosting service supports partial clone, then +we absolutely recommend it as a way to greatly speed up your clone and fetch +times and to reduce how much disk space your Git repository requires. Scalar +will help with this! + +Most of the value of Scalar can be found in the core Git client. However, most +of the advanced features that really optimize Git's performance are off by +default for compatibility reasons. To really take advantage of Git's latest and +greatest features, you either need to study the [`git config` +documentation](https://git-scm.com/docs/git-config) and regularly read [the Git +release notes](https://github.com/git/git/tree/master/Documentation/RelNotes). +Even if you do all that work and customize your Git settings on your machines, +you likely will want to share those settings with other team members. Or, you +can just use Scalar! + +Using `scalar register` on an existing Git repository will give you these +benefits: + +* Additional compression of your `.git/index` file. +* Hourly background `git fetch` operations, keeping you in-sync with your + remotes. +* Advanced data structures, such as the `commit-graph` and `multi-pack-index` + are updated automatically in the background. +* If using macOS or Windows, then Scalar configures Git's builtin File System + Monitor, providing faster commands such as `git status` or `git add`. + +Additionally, if you use `scalar clone` to create a new repository, then +you will automatically get these benefits: + +* Use Git's partial clone feature to only download the files you need for + your current checkout. +* Use Git's [sparse-checkout feature][sparse-checkout] to minimize the + number of files required in your working directory. + [Read more about sparse-checkout here.][sparse-checkout-blog] +* Create the Git repository inside `/src` to make it easy to + place build artifacts outside of the Git repository, such as in + `/bin` or `/packages`. + +We also admit that these **opinions** can always be improved! If you have +an idea of how to improve our setup, consider +[creating an issue](https://github.com/microsoft/scalar/issues/new) or +contributing a pull request! Some [existing](https://github.com/microsoft/scalar/issues/382) +[issues](https://github.com/microsoft/scalar/issues/388) have already +improved our configuration settings and roadmap! + +[gvfs-protocol]: https://github.com/microsoft/VFSForGit/blob/HEAD/Protocol.md +[microsoft-git]: https://github.com/microsoft/git +[sparse-checkout]: https://git-scm.com/docs/git-sparse-checkout +[sparse-checkout-blog]: https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/ diff --git a/contrib/scalar/docs/troubleshooting.md b/contrib/scalar/docs/troubleshooting.md new file mode 100644 index 00000000000000..8ec56ad437ff09 --- /dev/null +++ b/contrib/scalar/docs/troubleshooting.md @@ -0,0 +1,20 @@ +Troubleshooting +=============== + +Diagnosing Issues +----------------- + +The `scalar diagnose` command collects logs and config details for the current +repository. The resulting zip file helps root-cause issues. + +When run inside your repository, creates a zip file containing several important +files for that repository. This includes: + +* Configuration files from your `.git` folder, such as the `config` file, + `index`, `hooks`, and `refs`. + +* A summary of your Git object database, including the number of loose objects + and the names and sizes of pack-files. + +As the `diagnose` command completes, it provides the path of the resulting +zip file. This zip can be attached to bug reports to make the analysis easier. From c85e0cbe6bd1a167244e3243e619842d831be677 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 10 May 2022 13:43:05 +0200 Subject: [PATCH 061/229] scalar (Windows): use forward slashes as directory separators Git traditionally uses those, not backslashes, ever. Signed-off-by: Johannes Schindelin --- scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scalar.c b/scalar.c index 8308cbfd7dc487..a9672c1bb49fee 100644 --- a/scalar.c +++ b/scalar.c @@ -46,6 +46,9 @@ static void setup_enlistment_directory(int argc, const char **argv, die(_("need a working directory")); strbuf_trim_trailing_dir_sep(&path); +#ifdef GIT_WINDOWS_NATIVE + convert_slashes(path.buf); +#endif /* check if currently in enlistment root with src/ workdir */ len = path.len; From b90e1b216c7ffe1b9a81a47d9fdfa2c50773850f Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Thu, 17 Jun 2021 11:40:09 -0400 Subject: [PATCH 062/229] scalar: add retry logic to run_git() Use a fixed 3 tries total to see how that increases our chances of success for subcommands such as 'git fetch'. We special-case the `diagnose` command here: When 672196a3073 (scalar-diagnose: use 'git diagnose --mode=all', 2022-08-12) updated 'scalar diagnose' to run 'git diagnose' as a subprocess, it was passed through the run_git() caller. We need to avoid repeating the call when the underlying 'git diagnose' command fails. Signed-off-by: Derrick Stolee --- scalar.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scalar.c b/scalar.c index a9672c1bb49fee..6ee6d77ed2be88 100644 --- a/scalar.c +++ b/scalar.c @@ -75,21 +75,34 @@ static void setup_enlistment_directory(int argc, const char **argv, strbuf_release(&path); } +static int git_retries = 3; + LAST_ARG_MUST_BE_NULL static int run_git(const char *arg, ...) { - struct child_process cmd = CHILD_PROCESS_INIT; va_list args; const char *p; + struct strvec argv = STRVEC_INIT; + int res = 0, attempts; va_start(args, arg); - strvec_push(&cmd.args, arg); + strvec_push(&argv, arg); while ((p = va_arg(args, const char *))) - strvec_push(&cmd.args, p); + strvec_push(&argv, p); va_end(args); - cmd.git_cmd = 1; - return run_command(&cmd); + for (attempts = 0, res = 1; + res && attempts < git_retries; + attempts++) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushv(&cmd.args, argv.v); + res = run_command(&cmd); + } + + strvec_clear(&argv); + return res; } struct scalar_config { @@ -611,6 +624,8 @@ static int cmd_diagnose(int argc, const char **argv) setup_enlistment_directory(argc, argv, usage, options, &diagnostics_root); strbuf_addstr(&diagnostics_root, "/.scalarDiagnostics"); + /* Here, a failure should not repeat itself. */ + git_retries = 1; res = run_git("diagnose", "--mode=all", "-s", "%Y%m%d_%H%M%S", "-o", diagnostics_root.buf, NULL); From a1cef3acf8a92029277e99e6055bd8766786f8c6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 27 May 2021 07:26:11 +0200 Subject: [PATCH 063/229] scalar: support the `config` command for backwards compatibility The .NET version supported running `scalar config` to reconfigure the current enlistment, and now the C port does, too. Signed-off-by: Johannes Schindelin --- scalar.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scalar.c b/scalar.c index 6ee6d77ed2be88..a041efae8fbec0 100644 --- a/scalar.c +++ b/scalar.c @@ -1060,6 +1060,9 @@ int cmd_main(int argc, const char **argv) argv++; argc--; + if (!strcmp(argv[0], "config")) + argv[0] = "reconfigure"; + for (i = 0; builtins[i].name; i++) if (!strcmp(builtins[i].name, argv[0])) return !!builtins[i].fn(argc, argv); From 8b35499f3f7e6637701ee90b8e318a986e56169f Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Thu, 23 Sep 2021 09:59:06 -0400 Subject: [PATCH 064/229] TO-UPSTREAM: sequencer: avoid progress when stderr is redirected During a run of the Scalar functional tests, we hit a case where the inexact rename detection of a 'git cherry-pick' command slowed to the point of writing its delayed progress, failing the test because stderr differed from the control case. Showing progress like this when stderr is not a terminal is non-standard for Git, so inject an isatty(2) when initializing the progress option in sequencer.c. Unfortunately, there is no '--quiet' option in 'git cherry-pick' currently wired up. This could be considered in the future, and the isatty(2) could be moved to that position. This would also be needed for commands like 'git rebase', so we leave that for another time. Reported-by: Victoria Dye Signed-off-by: Derrick Stolee --- sequencer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 5476d39ba9b097..fc8c747c5424c6 100644 --- a/sequencer.c +++ b/sequencer.c @@ -765,7 +765,7 @@ static int do_recursive_merge(struct repository *r, o.branch2 = next ? next_label : "(empty tree)"; if (is_rebase_i(opts)) o.buffer_output = 2; - o.show_rename_progress = 1; + o.show_rename_progress = isatty(2); head_tree = parse_tree_indirect(head); if (!head_tree) From ea13c03262b0f2a3cfa3c705da96be0d0489681c Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Tue, 24 May 2016 00:32:38 +0000 Subject: [PATCH 065/229] gvfs: add global command pre and post hook procs This adds hard-coded call to GVFS.hooks.exe before and after each Git command runs. To make sure that this is only called on repositories cloned with GVFS, we test for the tell-tale .gvfs. 2021-10-30: Recent movement of find_hook() to hook.c required moving these changes out of run-command.c to hook.c. 2025-11-06: The `warn_on_auto_comment_char` hack is so ugly that it forces us to pile similarly ugly code on top because that hack _expects_ that the config has not been read when `cmd_commit()`, `cmd_revert()`, `cmd_cherry_pick()`, `cmd_merge()`, or `cmd_rebase()` set that flag. But with the `pre_command()` hook already run, that assumption is incorrect. Signed-off-by: Ben Peart Signed-off-by: Johannes Schindelin --- builtin/commit.c | 1 + builtin/merge.c | 1 + builtin/rebase.c | 1 + builtin/revert.c | 3 ++ git.c | 84 ++++++++++++++++++++++++++++++++++-- hook.c | 58 ++++++++++++++++++++++++- t/meson.build | 2 + t/t0400-pre-command-hook.sh | 34 +++++++++++++++ t/t0401-post-command-hook.sh | 32 ++++++++++++++ 9 files changed, 212 insertions(+), 4 deletions(-) create mode 100755 t/t0400-pre-command-hook.sh create mode 100755 t/t0401-post-command-hook.sh diff --git a/builtin/commit.c b/builtin/commit.c index a7077bacb0fca5..4f77abc43294f9 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1801,6 +1801,7 @@ int cmd_commit(int argc, #ifndef WITH_BREAKING_CHANGES warn_on_auto_comment_char = true; + repo_config_clear(the_repository); #endif /* !WITH_BREAKING_CHANGES */ prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/merge.c b/builtin/merge.c index c421a11b0b69df..f8d28a2b34ae4e 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1381,6 +1381,7 @@ int cmd_merge(int argc, #ifndef WITH_BREAKING_CHANGES warn_on_auto_comment_char = true; + repo_config_clear(the_repository); #endif /* !WITH_BREAKING_CHANGES */ prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/rebase.c b/builtin/rebase.c index c46882818982aa..6554730fca2380 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1243,6 +1243,7 @@ int cmd_rebase(int argc, #ifndef WITH_BREAKING_CHANGES warn_on_auto_comment_char = true; + repo_config_clear(the_repository); #endif /* !WITH_BREAKING_CHANGES */ prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/revert.c b/builtin/revert.c index bedc40f368eccc..bc50a8a51a5b5f 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -10,6 +10,7 @@ #include "rerere.h" #include "sequencer.h" #include "branch.h" +#include "config.h" /* * This implements the builtins revert and cherry-pick. @@ -288,6 +289,7 @@ int cmd_revert(int argc, #ifndef WITH_BREAKING_CHANGES warn_on_auto_comment_char = true; + repo_config_clear(the_repository); #endif /* !WITH_BREAKING_CHANGES */ opts.action = REPLAY_REVERT; sequencer_init_config(&opts); @@ -308,6 +310,7 @@ struct repository *repo UNUSED) #ifndef WITH_BREAKING_CHANGES warn_on_auto_comment_char = true; + repo_config_clear(the_repository); #endif /* !WITH_BREAKING_CHANGES */ opts.action = REPLAY_PICK; sequencer_init_config(&opts); diff --git a/git.c b/git.c index 0915bc643fe1c4..ffc8f111011879 100644 --- a/git.c +++ b/git.c @@ -17,6 +17,8 @@ #include "shallow.h" #include "trace.h" #include "trace2.h" +#include "dir.h" +#include "hook.h" #define RUN_SETUP (1<<0) #define RUN_SETUP_GENTLY (1<<1) @@ -463,6 +465,67 @@ static int handle_alias(struct strvec *args, struct string_list *expanded_aliase return ret; } +/* Runs pre/post-command hook */ +static struct strvec sargv = STRVEC_INIT; +static int run_post_hook = 0; +static int exit_code = -1; + +static int run_pre_command_hook(struct repository *r, const char **argv) +{ + char *lock; + int ret = 0; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + + /* + * Ensure the global pre/post command hook is only called for + * the outer command and not when git is called recursively + * or spawns multiple commands (like with the alias command) + */ + lock = getenv("COMMAND_HOOK_LOCK"); + if (lock && !strcmp(lock, "true")) + return 0; + setenv("COMMAND_HOOK_LOCK", "true", 1); + + /* call the hook proc */ + strvec_pushv(&sargv, argv); + strvec_pushv(&opt.args, sargv.v); + ret = run_hooks_opt(r, "pre-command", &opt); + + if (!ret) + run_post_hook = 1; + return ret; +} + +static int run_post_command_hook(struct repository *r) +{ + char *lock; + int ret = 0; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + + /* + * Only run post_command if pre_command succeeded in this process + */ + if (!run_post_hook) + return 0; + lock = getenv("COMMAND_HOOK_LOCK"); + if (!lock || strcmp(lock, "true")) + return 0; + + strvec_pushv(&opt.args, sargv.v); + strvec_pushf(&opt.args, "--exit_code=%u", exit_code); + ret = run_hooks_opt(r, "post-command", &opt); + + run_post_hook = 0; + strvec_clear(&sargv); + setenv("COMMAND_HOOK_LOCK", "false", 1); + return ret; +} + +static void post_command_hook_atexit(void) +{ + run_post_command_hook(the_repository); +} + static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct repository *repo) { int status, help; @@ -499,16 +562,21 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct if (!help && p->option & NEED_WORK_TREE) setup_work_tree(); + if (run_pre_command_hook(the_repository, argv)) + die("pre-command hook aborted command"); + trace_argv_printf(argv, "trace: built-in: git"); trace2_cmd_name(p->cmd); validate_cache_entries(repo->index); - status = p->fn(argc, argv, prefix, no_repo ? NULL : repo); + exit_code = status = p->fn(argc, argv, prefix, no_repo ? NULL : repo); validate_cache_entries(repo->index); if (status) return status; + run_post_command_hook(the_repository); + /* Somebody closed stdout? */ if (fstat(fileno(stdout), &st)) return 0; @@ -808,13 +876,16 @@ static void execv_dashed_external(const char **argv) */ trace_argv_printf(cmd.args.v, "trace: exec:"); + if (run_pre_command_hook(the_repository, cmd.args.v)) + die("pre-command hook aborted command"); + /* * If we fail because the command is not found, it is * OK to return. Otherwise, we just pass along the status code, * or our usual generic code if we were not even able to exec * the program. */ - status = run_command(&cmd); + exit_code = status = run_command(&cmd); /* * If the child process ran and we are now going to exit, emit a @@ -825,6 +896,8 @@ static void execv_dashed_external(const char **argv) exit(status); else if (errno != ENOENT) exit(128); + + run_post_command_hook(the_repository); } static int is_deprecated_command(const char *cmd) @@ -930,6 +1003,7 @@ int cmd_main(int argc, const char **argv) } trace_command_performance(argv); + atexit(post_command_hook_atexit); /* * "git-xxxx" is the same as "git xxxx", but we obviously: @@ -957,10 +1031,14 @@ int cmd_main(int argc, const char **argv) if (!argc) { /* The user didn't specify a command; give them help */ commit_pager_choice(); + if (run_pre_command_hook(the_repository, argv)) + die("pre-command hook aborted command"); printf(_("usage: %s\n\n"), git_usage_string); list_common_cmds_help(); printf("\n%s\n", _(git_more_info_string)); - exit(1); + exit_code = 1; + run_post_command_hook(the_repository); + exit(exit_code); } if (!strcmp("--version", argv[0]) || !strcmp("-v", argv[0])) diff --git a/hook.c b/hook.c index b3de1048bf44b9..be04f1a6be4142 100644 --- a/hook.c +++ b/hook.c @@ -1,5 +1,8 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "abspath.h" +#include "environment.h" #include "advice.h" #include "gettext.h" #include "hook.h" @@ -10,13 +13,66 @@ #include "environment.h" #include "setup.h" +static int early_hooks_path_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, void *cb) +{ + if (!strcmp(var, "core.hookspath")) + return git_config_pathname((char **)cb, var, value); + + return 0; +} + +/* Discover the hook before setup_git_directory() was called */ +static const char *hook_path_early(const char *name, struct strbuf *result) +{ + static struct strbuf hooks_dir = STRBUF_INIT; + static int initialized; + + if (initialized < 0) + return NULL; + + if (!initialized) { + struct strbuf gitdir = STRBUF_INIT, commondir = STRBUF_INIT; + char *early_hooks_dir = NULL; + + if (discover_git_directory(&commondir, &gitdir) < 0) { + strbuf_release(&gitdir); + strbuf_release(&commondir); + initialized = -1; + return NULL; + } + + read_early_config(the_repository, early_hooks_path_config, &early_hooks_dir); + if (!early_hooks_dir) + strbuf_addf(&hooks_dir, "%s/hooks/", commondir.buf); + else { + strbuf_add_absolute_path(&hooks_dir, early_hooks_dir); + free(early_hooks_dir); + strbuf_addch(&hooks_dir, '/'); + } + + strbuf_release(&gitdir); + strbuf_release(&commondir); + + initialized = 1; + } + + strbuf_addf(result, "%s%s", hooks_dir.buf, name); + return result->buf; +} + const char *find_hook(struct repository *r, const char *name) { static struct strbuf path = STRBUF_INIT; int found_hook; - repo_git_path_replace(r, &path, "hooks/%s", name); + strbuf_reset(&path); + if (have_git_dir()) + repo_git_path_replace(r, &path, "hooks/%s", name); + else if (!hook_path_early(name, &path)) + return NULL; + found_hook = access(path.buf, X_OK) >= 0; #ifdef STRIP_EXTENSION if (!found_hook) { diff --git a/t/meson.build b/t/meson.build index 2b2d85124ac3f7..dbb5f746198ec0 100644 --- a/t/meson.build +++ b/t/meson.build @@ -136,6 +136,8 @@ integration_tests = [ 't0301-credential-cache.sh', 't0302-credential-store.sh', 't0303-credential-external.sh', + 't0400-pre-command-hook.sh', + 't0401-post-command-hook.sh', 't0410-partial-clone.sh', 't0411-clone-from-partial.sh', 't0450-txt-doc-vs-help.sh', diff --git a/t/t0400-pre-command-hook.sh b/t/t0400-pre-command-hook.sh new file mode 100755 index 00000000000000..4f4f610b52b0a0 --- /dev/null +++ b/t/t0400-pre-command-hook.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +test_description='pre-command hook' + +. ./test-lib.sh + +test_expect_success 'with no hook' ' + echo "first" > file && + git add file && + git commit -m "first" +' + +test_expect_success 'with succeeding hook' ' + mkdir -p .git/hooks && + write_script .git/hooks/pre-command <<-EOF && + echo "\$*" >\$(git rev-parse --git-dir)/pre-command.out + EOF + echo "second" >> file && + git add file && + test "add file" = "$(cat .git/pre-command.out)" && + echo Hello | git hash-object --stdin && + test "hash-object --stdin" = "$(cat .git/pre-command.out)" +' + +test_expect_success 'with failing hook' ' + write_script .git/hooks/pre-command <<-EOF && + exit 1 + EOF + echo "third" >> file && + test_must_fail git add file && + test_path_is_missing "$(cat .git/pre-command.out)" +' + +test_done diff --git a/t/t0401-post-command-hook.sh b/t/t0401-post-command-hook.sh new file mode 100755 index 00000000000000..64646f7ad03b57 --- /dev/null +++ b/t/t0401-post-command-hook.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +test_description='post-command hook' + +. ./test-lib.sh + +test_expect_success 'with no hook' ' + echo "first" > file && + git add file && + git commit -m "first" +' + +test_expect_success 'with succeeding hook' ' + mkdir -p .git/hooks && + write_script .git/hooks/post-command <<-EOF && + echo "\$*" >\$(git rev-parse --git-dir)/post-command.out + EOF + echo "second" >> file && + git add file && + test "add file --exit_code=0" = "$(cat .git/post-command.out)" +' + +test_expect_success 'with failing pre-command hook' ' + write_script .git/hooks/pre-command <<-EOF && + exit 1 + EOF + echo "third" >> file && + test_must_fail git add file && + test_path_is_missing "$(cat .git/post-command.out)" +' + +test_done From d35ee8dba5ef4a87786b570af989b3613aae6f4a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 16 Mar 2017 21:07:54 +0100 Subject: [PATCH 066/229] t0400: verify that the hook is called correctly from a subdirectory Suggested by Ben Peart. Signed-off-by: Johannes Schindelin --- t/t0400-pre-command-hook.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/t/t0400-pre-command-hook.sh b/t/t0400-pre-command-hook.sh index 4f4f610b52b0a0..83c453c9643eae 100755 --- a/t/t0400-pre-command-hook.sh +++ b/t/t0400-pre-command-hook.sh @@ -31,4 +31,27 @@ test_expect_success 'with failing hook' ' test_path_is_missing "$(cat .git/pre-command.out)" ' +test_expect_success 'in a subdirectory' ' + echo touch i-was-here | write_script .git/hooks/pre-command && + mkdir sub && + ( + cd sub && + git version + ) && + test_path_is_file sub/i-was-here +' + +test_expect_success 'in a subdirectory, using an alias' ' + git reset --hard && + echo "echo \"\$@; \$(pwd)\" >>log" | + write_script .git/hooks/pre-command && + mkdir -p sub && + ( + cd sub && + git -c alias.v="version" v + ) && + test_path_is_missing log && + test_line_count = 2 sub/log +' + test_done From 8a72bf7cab3970a00ad16b4b1420d17fa2416322 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 12 Mar 2025 12:46:24 +0000 Subject: [PATCH 067/229] t0400: verify core.hooksPath is respected by pre-command Verify that the core.hooksPath configuration is repsected by the pre-command hook. Original regression test was written by Alejandro Pauly. Signed-off-by: Matthew John Cheetham --- t/t0400-pre-command-hook.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t0400-pre-command-hook.sh b/t/t0400-pre-command-hook.sh index 83c453c9643eae..9c9f435662e6b3 100755 --- a/t/t0400-pre-command-hook.sh +++ b/t/t0400-pre-command-hook.sh @@ -54,4 +54,15 @@ test_expect_success 'in a subdirectory, using an alias' ' test_line_count = 2 sub/log ' +test_expect_success 'with core.hooksPath' ' + mkdir -p .git/alternateHooks && + write_script .git/alternateHooks/pre-command <<-EOF && + echo "alternate" >\$(git rev-parse --git-dir)/pre-command.out + EOF + write_script .git/hooks/pre-command <<-EOF && + echo "original" >\$(git rev-parse --git-dir)/pre-command.out + EOF + git -c core.hooksPath=.git/alternateHooks status && + test "alternate" = "$(cat .git/pre-command.out)" +' test_done From 91f0c827d5b71da962600b2adef0a6a035ee657b Mon Sep 17 00:00:00 2001 From: Alejandro Pauly Date: Mon, 10 Apr 2017 13:26:14 -0400 Subject: [PATCH 068/229] Pass PID of git process to hooks. Signed-off-by: Alejandro Pauly --- git.c | 1 + t/t0400-pre-command-hook.sh | 3 ++- t/t0401-post-command-hook.sh | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/git.c b/git.c index ffc8f111011879..a52b94a78b3c2a 100644 --- a/git.c +++ b/git.c @@ -488,6 +488,7 @@ static int run_pre_command_hook(struct repository *r, const char **argv) /* call the hook proc */ strvec_pushv(&sargv, argv); + strvec_pushf(&sargv, "--git-pid=%"PRIuMAX, (uintmax_t)getpid()); strvec_pushv(&opt.args, sargv.v); ret = run_hooks_opt(r, "pre-command", &opt); diff --git a/t/t0400-pre-command-hook.sh b/t/t0400-pre-command-hook.sh index 9c9f435662e6b3..34aed1711459bd 100755 --- a/t/t0400-pre-command-hook.sh +++ b/t/t0400-pre-command-hook.sh @@ -13,7 +13,8 @@ test_expect_success 'with no hook' ' test_expect_success 'with succeeding hook' ' mkdir -p .git/hooks && write_script .git/hooks/pre-command <<-EOF && - echo "\$*" >\$(git rev-parse --git-dir)/pre-command.out + echo "\$*" | sed "s/ --git-pid=[0-9]*//" \ + >\$(git rev-parse --git-dir)/pre-command.out EOF echo "second" >> file && git add file && diff --git a/t/t0401-post-command-hook.sh b/t/t0401-post-command-hook.sh index 64646f7ad03b57..fcbfc4a0c79c1e 100755 --- a/t/t0401-post-command-hook.sh +++ b/t/t0401-post-command-hook.sh @@ -13,7 +13,8 @@ test_expect_success 'with no hook' ' test_expect_success 'with succeeding hook' ' mkdir -p .git/hooks && write_script .git/hooks/post-command <<-EOF && - echo "\$*" >\$(git rev-parse --git-dir)/post-command.out + echo "\$*" | sed "s/ --git-pid=[0-9]*//" \ + >\$(git rev-parse --git-dir)/post-command.out EOF echo "second" >> file && git add file && From b7a7125bc885b2a8ac2692c167a6b60b771795d2 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Wed, 22 Feb 2017 12:50:43 -0700 Subject: [PATCH 069/229] sparse-checkout: make sure to update files with a modify/delete conflict When using the sparse-checkout feature, the file might not be on disk because the skip-worktree bit is on. This used to be a bug in the (hence deleted) `recursive` strategy. Let's ensure that this bug does not resurface. Signed-off-by: Kevin Willford Signed-off-by: Johannes Schindelin --- t/meson.build | 1 + t/t7616-merge-sparse-checkout.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100755 t/t7616-merge-sparse-checkout.sh diff --git a/t/meson.build b/t/meson.build index dbb5f746198ec0..95c4a0bbf0f380 100644 --- a/t/meson.build +++ b/t/meson.build @@ -936,6 +936,7 @@ integration_tests = [ 't7612-merge-verify-signatures.sh', 't7614-merge-signoff.sh', 't7615-diff-algo-with-mergy-operations.sh', + 't7616-merge-sparse-checkout.sh', 't7700-repack.sh', 't7701-repack-unpack-unreachable.sh', 't7702-repack-cyclic-alternate.sh', diff --git a/t/t7616-merge-sparse-checkout.sh b/t/t7616-merge-sparse-checkout.sh new file mode 100755 index 00000000000000..5ce12431f62ad1 --- /dev/null +++ b/t/t7616-merge-sparse-checkout.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +test_description='merge can handle sparse-checkout' + +. ./test-lib.sh + +# merges with conflicts + +test_expect_success 'setup' ' + git branch -M main && + test_commit a && + test_commit file && + git checkout -b delete-file && + git rm file.t && + test_tick && + git commit -m "remove file" && + git checkout main && + test_commit modify file.t changed +' + +test_expect_success 'merge conflict deleted file and modified' ' + echo "/a.t" >.git/info/sparse-checkout && + test_config core.sparsecheckout true && + git checkout -f && + test_path_is_missing file.t && + test_must_fail git merge delete-file && + test_path_is_file file.t && + test "changed" = "$(cat file.t)" +' + +test_done From a2c1d35a299e9d7fb1e80b11778e8b640cce728c Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Fri, 30 Sep 2022 12:59:40 -0400 Subject: [PATCH 070/229] worktree: allow in Scalar repositories The 'git worktree' command was marked as BLOCK_ON_GVFS_REPO because it does not interact well with the virtual filesystem of VFS for Git. When a Scalar clone uses the GVFS protocol, it enables the GVFS_BLOCK_COMMANDS flag, since commands like 'git gc' do not work well with the GVFS protocol. However, 'git worktree' works just fine with the GVFS protocol since it isn't doing anything special. It copies the sparse-checkout from the current worktree, so it does not have performance issues. This is a highly requested option. The solution is to stop using the BLOCK_ON_GVFS_REPO option and instead add a special-case check in cmd_worktree() specifically for a particular bit of the 'core_gvfs' global variable (loaded by very early config reading) that corresponds to the virtual filesystem. The bit that most closely resembled this behavior was non-obviously named, but does provide a signal that we are in a Scalar clone and not a VFS for Git clone. The error message is copied from git.c, so it will have the same output as before if a user runs this in a VFS for Git clone. Signed-off-by: Derrick Stolee --- builtin/worktree.c | 8 ++++++++ gvfs.h | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/builtin/worktree.c b/builtin/worktree.c index 812774a5ca992c..6877363a40ad14 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -4,6 +4,7 @@ #include "builtin.h" #include "abspath.h" #include "advice.h" +#include "gvfs.h" #include "checkout.h" #include "config.h" #include "copy.h" @@ -1450,6 +1451,13 @@ int cmd_worktree(int ac, repo_config(the_repository, git_worktree_config, NULL); + /* + * git-worktree is special-cased to work in Scalar repositories + * even when they use the GVFS Protocol. + */ + if (gvfs_config_is_set(repo, GVFS_USE_VIRTUAL_FILESYSTEM)) + die("'git %s' is not supported on a GVFS repo", "worktree"); + if (!prefix) prefix = ""; diff --git a/gvfs.h b/gvfs.h index e3995b586cddd5..59ee3893b1efa6 100644 --- a/gvfs.h +++ b/gvfs.h @@ -13,7 +13,18 @@ struct repository; */ #define GVFS_SKIP_SHA_ON_INDEX (1 << 0) #define GVFS_MISSING_OK (1 << 2) + +/* + * This behavior of not deleting outside of the sparse-checkout + * is specific to the virtual filesystem support. It is only + * enabled by VFS for Git, and so can be used as an indicator + * that we are in a virtualized filesystem environment and not + * in a Scalar environment. This bit has two names to reflect + * that. + */ #define GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT (1 << 3) +#define GVFS_USE_VIRTUAL_FILESYSTEM (1 << 3) + #define GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK (1 << 4) #define GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS (1 << 6) From d20629283c30a7c49df8f758e4d4d17ad143fe38 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Wed, 1 Mar 2017 15:17:12 -0800 Subject: [PATCH 071/229] sparse-checkout: avoid writing entries with the skip-worktree bit When using the sparse-checkout feature git should not write to the working directory for files with the skip-worktree bit on. With the skip-worktree bit on the file may or may not be in the working directory and if it is not we don't want or need to create it by calling checkout_entry. There are two callers of checkout_target. Both of which check that the file does not exist before calling checkout_target. load_current which make a call to lstat right before calling checkout_target and check_preimage which will only run checkout_taret it stat_ret is less than zero. It sets stat_ret to zero and only if !stat->cached will it lstat the file and set stat_ret to something other than zero. This patch checks if skip-worktree bit is on in checkout_target and just returns so that the entry doesn't not end up in the working directory. This is so that apply will not create a file in the working directory, then update the index but not keep the working directory up to date with the changes that happened in the index. Signed-off-by: Kevin Willford --- apply.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apply.c b/apply.c index bf1260b51d2c2d..84e8d4f13c3536 100644 --- a/apply.c +++ b/apply.c @@ -20,6 +20,7 @@ #include "dir.h" #include "environment.h" #include "gettext.h" +#include "gvfs.h" #include "hex.h" #include "xdiff-interface.h" #include "merge-ll.h" @@ -3373,6 +3374,25 @@ static int checkout_target(struct index_state *istate, { struct checkout costate = CHECKOUT_INIT; + /* + * Do not checkout the entry if the skipworktree bit is set + * + * Both callers of this method (check_preimage and load_current) + * check for the existance of the file before calling this + * method so we know that the file doesn't exist at this point + * and we don't need to perform that check again here. + * We just need to check the skip-worktree and return. + * + * This is to prevent git from creating a file in the + * working directory that has the skip-worktree bit on, + * then updating the index from the patch and not keeping + * the working directory version up to date with what it + * changed the index version to be. + */ + if (gvfs_config_is_set(istate->repo, GVFS_USE_VIRTUAL_FILESYSTEM) && + ce_skip_worktree(ce)) + return 0; + costate.refresh_cache = 1; costate.istate = istate; if (checkout_entry(ce, &costate, NULL, NULL) || From e54a14301464b7dfe96cf17bb40563db248eefcf Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Wed, 5 Apr 2017 10:55:32 -0600 Subject: [PATCH 072/229] Do not remove files outside the sparse-checkout Signed-off-by: Kevin Willford --- unpack-trees.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unpack-trees.c b/unpack-trees.c index 5c74b5374dc67a..84398d257de278 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -574,7 +574,9 @@ static int apply_sparse_checkout(struct index_state *istate, ce->ce_flags &= ~CE_SKIP_WORKTREE; return -1; } - ce->ce_flags |= CE_WT_REMOVE; + if (!gvfs_config_is_set(istate->repo, GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT)) + ce->ce_flags |= CE_WT_REMOVE; + ce->ce_flags &= ~CE_UPDATE; } if (was_skip_worktree && !ce_skip_worktree(ce)) { From bdd04778dba8dac6493bf6f85fc07ec3d6a65c0b Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Fri, 16 Nov 2018 11:28:59 -0700 Subject: [PATCH 073/229] send-pack: do not check for sha1 file when GVFS_MISSING_OK set --- send-pack.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/send-pack.c b/send-pack.c index 22a1beed8d9823..701083dfad38e8 100644 --- a/send-pack.c +++ b/send-pack.c @@ -3,6 +3,7 @@ #include "commit.h" #include "date.h" #include "gettext.h" +#include "gvfs.h" #include "hex.h" #include "odb.h" #include "pkt-line.h" @@ -45,7 +46,7 @@ int option_parse_push_signed(const struct option *opt, static void feed_object(struct repository *r, const struct object_id *oid, FILE *fh, int negative) { - if (negative && !odb_has_object(r->objects, oid, 0)) + if (negative && !gvfs_config_is_set(r, GVFS_MISSING_OK) && !odb_has_object(r->objects, oid, 0)) return; if (negative) From 43206508a8d5cf78f8e4d32ef081259f36e777b4 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 6 Mar 2025 11:12:35 +0000 Subject: [PATCH 074/229] git.c: add VFS enabled cmd blocking Add the ability to block built-in commands based on if the `core.gvfs` setting has the `GVFS_USE_VIRTUAL_FILESYSTEM` bit set. This allows us to selectively block commands that use the GVFS protocol, but don't use VFS for Git (for example repos cloned via `scalar clone` against Azure DevOps). Signed-off-by: Matthew John Cheetham --- git.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git.c b/git.c index 252a4d6d358dcb..0d1168166ef504 100644 --- a/git.c +++ b/git.c @@ -33,6 +33,7 @@ #define NO_PARSEOPT (1<<5) /* parse-options is not used */ #define DEPRECATED (1<<6) #define BLOCK_ON_GVFS_REPO (1<<7) /* command not allowed in GVFS repos */ +#define BLOCK_ON_VFS_ENABLED (1<<8) /* command not allowed when virtual file system is used */ struct cmd_struct { const char *cmd; @@ -568,6 +569,9 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct if (!help && p->option & BLOCK_ON_GVFS_REPO && gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) die("'git %s' is not supported on a GVFS repo", p->cmd); + if (!help && p->option & BLOCK_ON_VFS_ENABLED && gvfs_config_is_set(repo, GVFS_USE_VIRTUAL_FILESYSTEM)) + die("'git %s' is not supported when using the virtual file system", p->cmd); + if (run_pre_command_hook(the_repository, argv)) die("pre-command hook aborted command"); From 35c23ee96e39fc25a1317faf585d678c6dbced6a Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Thu, 11 Jan 2018 16:25:08 -0500 Subject: [PATCH 075/229] Add virtual file system settings and hook proc On index load, clear/set the skip worktree bits based on the virtual file system data. Use virtual file system data to update skip-worktree bit in unpack-trees. Use virtual file system data to exclude files and folders not explicitly requested. Update 2022-04-05: disable the "present-despite-SKIP_WORKTREE" file removal behavior when 'core.virtualfilesystem' is enabled. Signed-off-by: Ben Peart --- Documentation/config/core.adoc | 8 + Documentation/githooks.adoc | 20 ++ Makefile | 1 + config.c | 26 +++ config.h | 2 + dir.c | 43 +++- environment.c | 7 +- environment.h | 1 + meson.build | 1 + read-cache.c | 2 + sparse-index.c | 5 +- t/meson.build | 1 + t/t1090-sparse-checkout-scope.sh | 4 +- t/t1093-virtualfilesystem.sh | 369 +++++++++++++++++++++++++++++++ unpack-trees.c | 14 +- virtualfilesystem.c | 312 ++++++++++++++++++++++++++ virtualfilesystem.h | 25 +++ wt-status.c | 2 + 18 files changed, 835 insertions(+), 8 deletions(-) create mode 100755 t/t1093-virtualfilesystem.sh create mode 100644 virtualfilesystem.c create mode 100644 virtualfilesystem.h diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 52133689ebafae..e77d637182bc2c 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -111,6 +111,14 @@ Version 2 uses an opaque string so that the monitor can return something that can be used to determine what files have changed without race conditions. +core.virtualFilesystem:: + If set, the value of this variable is used as a command which + will identify all files and directories that are present in + the working directory. Git will only track and update files + listed in the virtual file system. Using the virtual file system + will supersede the sparse-checkout settings which will be ignored. + See the "virtual file system" section of linkgit:githooks[5]. + core.trustctime:: If false, the ctime differences between the index and the working tree are ignored; useful when the inode change time diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc index 0397dec64d7315..86c78b3b4825e2 100644 --- a/Documentation/githooks.adoc +++ b/Documentation/githooks.adoc @@ -758,6 +758,26 @@ and "0" meaning they were not. Only one parameter should be set to "1" when the hook runs. The hook running passing "1", "1" should not be possible. +virtualFilesystem +~~~~~~~~~~~~~~~~~~ + +"Virtual File System" allows populating the working directory sparsely. +The projection data is typically automatically generated by an external +process. Git will limit what files it checks for changes as well as which +directories are checked for untracked files based on the path names given. +Git will also only update those files listed in the projection. + +The hook is invoked when the configuration option core.virtualFilesystem +is set. It takes one argument, a version (currently 1). + +The hook should output to stdout the list of all files in the working +directory that git should track. The paths are relative to the root +of the working directory and are separated by a single NUL. Full paths +('dir1/a.txt') as well as directories are supported (ie 'dir1/'). + +The exit status determines whether git will use the data from the +hook. On error, git will abort the command with an error message. + SEE ALSO -------- linkgit:git-hook[1] diff --git a/Makefile b/Makefile index 604ae7bd63ef0a..4c25827c184b86 100644 --- a/Makefile +++ b/Makefile @@ -1345,6 +1345,7 @@ LIB_OBJS += varint.o endif LIB_OBJS += version.o LIB_OBJS += versioncmp.o +LIB_OBJS += virtualfilesystem.o LIB_OBJS += walker.o LIB_OBJS += wildmatch.o LIB_OBJS += worktree.o diff --git a/config.c b/config.c index 9abda809f69e20..53ff9958e9c130 100644 --- a/config.c +++ b/config.c @@ -2479,6 +2479,32 @@ int repo_config_get_max_percent_split_change(struct repository *r) return -1; /* default value */ } +int repo_config_get_virtualfilesystem(struct repository *r) +{ + /* Run only once. */ + static int virtual_filesystem_result = -1; + extern char *core_virtualfilesystem; + extern int core_apply_sparse_checkout; + if (virtual_filesystem_result >= 0) + return virtual_filesystem_result; + + if (repo_config_get_pathname(r, "core.virtualfilesystem", &core_virtualfilesystem)) + core_virtualfilesystem = xstrdup_or_null(getenv("GIT_VIRTUALFILESYSTEM_TEST")); + + if (core_virtualfilesystem && !*core_virtualfilesystem) + FREE_AND_NULL(core_virtualfilesystem); + + /* virtual file system relies on the sparse checkout logic so force it on */ + if (core_virtualfilesystem) { + core_apply_sparse_checkout = 1; + virtual_filesystem_result = 1; + return 1; + } + + virtual_filesystem_result = 0; + return 0; +} + int repo_config_get_index_threads(struct repository *r, int *dest) { int is_bool, val; diff --git a/config.h b/config.h index 19c87fc0bc1a2a..fc5b2d2e22dc09 100644 --- a/config.h +++ b/config.h @@ -686,6 +686,8 @@ int repo_config_get_index_threads(struct repository *r, int *dest); int repo_config_get_split_index(struct repository *r); int repo_config_get_max_percent_split_change(struct repository *r); +int repo_config_get_virtualfilesystem(struct repository *r); + /* This dies if the configured or default date is in the future */ int repo_config_get_expiry(struct repository *r, const char *key, char **output); diff --git a/dir.c b/dir.c index 186b8832db98fa..a62a683866a635 100644 --- a/dir.c +++ b/dir.c @@ -11,6 +11,7 @@ #include "git-compat-util.h" #include "abspath.h" +#include "virtualfilesystem.h" #include "config.h" #include "convert.h" #include "dir.h" @@ -1533,6 +1534,19 @@ enum pattern_match_result path_matches_pattern_list( int result = NOT_MATCHED; size_t slash_pos; + if (core_virtualfilesystem) { + /* + * The virtual file system data is used to prevent git from traversing + * any part of the tree that is not in the virtual file system. Return + * 1 to exclude the entry if it is not found in the virtual file system, + * else fall through to the regular excludes logic as it may further exclude. + */ + if (*dtype == DT_UNKNOWN) + *dtype = resolve_dtype(DT_UNKNOWN, istate, pathname, pathlen); + if (is_excluded_from_virtualfilesystem(pathname, pathlen, *dtype) > 0) + return 1; + } + if (!pl->use_cone_patterns) { pattern = last_matching_pattern_from_list(pathname, pathlen, basename, dtype, pl, istate); @@ -1622,6 +1636,13 @@ static int path_in_sparse_checkout_1(const char *path, enum pattern_match_result match = UNDECIDED; const char *end, *slash; + /* + * When using a virtual filesystem, there aren't really patterns + * to follow, but be extra careful to skip this check. + */ + if (core_virtualfilesystem) + return 1; + /* * We default to accepting a path if the path is empty, there are no * patterns, or the patterns are of the wrong type. @@ -1877,8 +1898,22 @@ struct path_pattern *last_matching_pattern(struct dir_struct *dir, int is_excluded(struct dir_struct *dir, struct index_state *istate, const char *pathname, int *dtype_p) { - struct path_pattern *pattern = - last_matching_pattern(dir, istate, pathname, dtype_p); + struct path_pattern *pattern; + + if (core_virtualfilesystem) { + /* + * The virtual file system data is used to prevent git from traversing + * any part of the tree that is not in the virtual file system. Return + * 1 to exclude the entry if it is not found in the virtual file system, + * else fall through to the regular excludes logic as it may further exclude. + */ + if (*dtype_p == DT_UNKNOWN) + *dtype_p = resolve_dtype(DT_UNKNOWN, istate, pathname, strlen(pathname)); + if (is_excluded_from_virtualfilesystem(pathname, strlen(pathname), *dtype_p) > 0) + return 1; + } + + pattern = last_matching_pattern(dir, istate, pathname, dtype_p); if (pattern) return pattern->flags & PATTERN_FLAG_NEGATIVE ? 0 : 1; return 0; @@ -2496,6 +2531,8 @@ static enum path_treatment treat_path(struct dir_struct *dir, ignore_case); if (dtype != DT_DIR && has_path_in_index) return path_none; + if (is_excluded_from_virtualfilesystem(path->buf, path->len, dtype) > 0) + return path_excluded; /* * When we are looking at a directory P in the working tree, @@ -2700,6 +2737,8 @@ static void add_path_to_appropriate_result_list(struct dir_struct *dir, /* add the path to the appropriate result list */ switch (state) { case path_excluded: + if (is_excluded_from_virtualfilesystem(path->buf, path->len, DT_DIR) > 0) + break; if (dir->flags & DIR_SHOW_IGNORED) dir_add_name(dir, istate, path->buf, path->len); else if ((dir->flags & DIR_SHOW_IGNORED_TOO) || diff --git a/environment.c b/environment.c index 85b68f0be8fa1b..721574f0dd9440 100644 --- a/environment.c +++ b/environment.c @@ -78,6 +78,7 @@ int grafts_keep_true_parents; int core_apply_sparse_checkout; int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; +char *core_virtualfilesystem; int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ unsigned long pack_size_limit_cfg; int core_virtualize_objects; @@ -558,7 +559,11 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.sparsecheckout")) { - core_apply_sparse_checkout = git_config_bool(var, value); + /* virtual file system relies on the sparse checkout logic so force it on */ + if (core_virtualfilesystem) + core_apply_sparse_checkout = 1; + else + core_apply_sparse_checkout = git_config_bool(var, value); return 0; } diff --git a/environment.h b/environment.h index aca212b53e8cd9..ce510ac461b63c 100644 --- a/environment.h +++ b/environment.h @@ -160,6 +160,7 @@ extern int pack_compression_level; extern unsigned long pack_size_limit_cfg; extern int max_allowed_tree_depth; +extern char *core_virtualfilesystem; extern int precomposed_unicode; extern int protect_hfs; extern int protect_ntfs; diff --git a/meson.build b/meson.build index 9be1056fc176ae..6876d78dfd77d5 100644 --- a/meson.build +++ b/meson.build @@ -532,6 +532,7 @@ libgit_sources = [ 'utf8.c', 'version.c', 'versioncmp.c', + 'virtualfilesystem.c', 'walker.c', 'wildmatch.c', 'worktree.c', diff --git a/read-cache.c b/read-cache.c index f5f486fafcb6dc..9ff9184ebf00a1 100644 --- a/read-cache.c +++ b/read-cache.c @@ -8,6 +8,7 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" +#include "virtualfilesystem.h" #include "config.h" #include "date.h" #include "diff.h" @@ -1952,6 +1953,7 @@ static void post_read_index_from(struct index_state *istate) tweak_untracked_cache(istate); tweak_split_index(istate); tweak_fsmonitor(istate); + apply_virtualfilesystem(istate); } static size_t estimate_cache_size_from_compressed(unsigned int entries) diff --git a/sparse-index.c b/sparse-index.c index 4d75ee428660ab..cab8c0366589ae 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -272,7 +272,7 @@ static int add_path_to_index(const struct object_id *oid, size_t len = base->len; if (S_ISDIR(mode)) { - int dtype; + int dtype = DT_DIR; size_t baselen = base->len; if (!ctx->pl) return READ_TREE_RECURSIVE; @@ -396,7 +396,7 @@ void expand_index(struct index_state *istate, struct pattern_list *pl) struct cache_entry *ce = istate->cache[i]; struct tree *tree; struct pathspec ps; - int dtype; + int dtype = DT_UNKNOWN; if (!S_ISSPARSEDIR(ce->ce_mode)) { set_index_entry(full, full->cache_nr++, ce); @@ -672,6 +672,7 @@ static void clear_skip_worktree_from_present_files_full(struct index_state *ista void clear_skip_worktree_from_present_files(struct index_state *istate) { if (!core_apply_sparse_checkout || + core_virtualfilesystem || sparse_expect_files_outside_of_patterns) return; diff --git a/t/meson.build b/t/meson.build index d53f4aa25ff576..cfb187c858458a 100644 --- a/t/meson.build +++ b/t/meson.build @@ -178,6 +178,7 @@ integration_tests = [ 't1090-sparse-checkout-scope.sh', 't1091-sparse-checkout-builtin.sh', 't1092-sparse-checkout-compatibility.sh', + 't1093-virtualfilesystem.sh', 't1100-commit-tree-options.sh', 't1300-config.sh', 't1301-shared-repo.sh', diff --git a/t/t1090-sparse-checkout-scope.sh b/t/t1090-sparse-checkout-scope.sh index effa20aab7bea7..02b393e36a7d28 100755 --- a/t/t1090-sparse-checkout-scope.sh +++ b/t/t1090-sparse-checkout-scope.sh @@ -107,9 +107,9 @@ test_expect_success 'in partial clone, sparse checkout only fetches needed blobs ' test_expect_success 'checkout does not delete items outside the sparse checkout file' ' - # The "sparse.expectfilesoutsideofpatterns" config will prevent the + # The "core.virtualfilesystem" config will prevent the # SKIP_WORKTREE flag from being dropped on files present on-disk. - test_config sparse.expectfilesoutsideofpatterns true && + test_config core.virtualfilesystem true && test_config core.gvfs 8 && git checkout -b outside && diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh new file mode 100755 index 00000000000000..bd0c9f72ba3c4a --- /dev/null +++ b/t/t1093-virtualfilesystem.sh @@ -0,0 +1,369 @@ +#!/bin/sh + +test_description='virtual file system tests' + +. ./test-lib.sh + +clean_repo () { + rm .git/index && + git -c core.virtualfilesystem= reset --hard HEAD && + git -c core.virtualfilesystem= clean -fd && + touch untracked.txt && + touch dir1/untracked.txt && + touch dir2/untracked.txt +} + +test_expect_success 'setup' ' + git branch -M main && + mkdir -p .git/hooks/ && + cat > .gitignore <<-\EOF && + .gitignore + expect* + actual* + EOF + mkdir -p dir1 && + touch dir1/file1.txt && + touch dir1/file2.txt && + mkdir -p dir2 && + touch dir2/file1.txt && + touch dir2/file2.txt && + git add . && + git commit -m "initial" && + git config --local core.virtualfilesystem .git/hooks/virtualfilesystem +' + +test_expect_success 'test hook parameters and version' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + if test "$#" -ne 1 + then + echo "$0: Exactly 1 argument expected" >&2 + exit 2 + fi + + if test "$1" != 1 + then + echo "$0: Unsupported hook version." >&2 + exit 1 + fi + EOF + git status && + write_script .git/hooks/virtualfilesystem <<-\EOF && + exit 3 + EOF + test_must_fail git status +' + +test_expect_success 'verify status is clean' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir2/file1.txt\0" + EOF + rm -f .git/index && + git checkout -f && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir2/file1.txt\0" + printf "dir1/file1.txt\0" + printf "dir1/file2.txt\0" + EOF + git status > actual && + cat > expected <<-\EOF && + On branch main + nothing to commit, working tree clean + EOF + test_cmp expected actual +' + +test_expect_success 'verify skip-worktree bit is set for absolute path' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + EOF + git ls-files -v > actual && + cat > expected <<-\EOF && + H dir1/file1.txt + S dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify skip-worktree bit is cleared for absolute path' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file2.txt\0" + EOF + git ls-files -v > actual && + cat > expected <<-\EOF && + S dir1/file1.txt + H dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify folder wild cards' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/\0" + EOF + git ls-files -v > actual && + cat > expected <<-\EOF && + H dir1/file1.txt + H dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify folders not included are ignored' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + printf "dir1/file2.txt\0" + EOF + mkdir -p dir1/dir2 && + touch dir1/a && + touch dir1/b && + touch dir1/dir2/a && + touch dir1/dir2/b && + git add . && + git ls-files -v > actual && + cat > expected <<-\EOF && + H dir1/file1.txt + H dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify including one file doesnt include the rest' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + printf "dir1/file2.txt\0" + printf "dir1/dir2/a\0" + EOF + mkdir -p dir1/dir2 && + touch dir1/a && + touch dir1/b && + touch dir1/dir2/a && + touch dir1/dir2/b && + git add . && + git ls-files -v > actual && + cat > expected <<-\EOF && + H dir1/dir2/a + H dir1/file1.txt + H dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify files not listed are ignored by git clean -f -x' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "untracked.txt\0" + printf "dir1/\0" + EOF + mkdir -p dir3 && + touch dir3/untracked.txt && + git clean -f -x && + test ! -f untracked.txt && + test -d dir1 && + test -f dir1/file1.txt && + test -f dir1/file2.txt && + test ! -f dir1/untracked.txt && + test -f dir2/file1.txt && + test -f dir2/file2.txt && + test -f dir2/untracked.txt && + test -d dir3 && + test -f dir3/untracked.txt +' + +test_expect_success 'verify files not listed are ignored by git clean -f -d -x' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "untracked.txt\0" + printf "dir1/\0" + printf "dir3/\0" + EOF + mkdir -p dir3 && + touch dir3/untracked.txt && + git clean -f -d -x && + test ! -f untracked.txt && + test -d dir1 && + test -f dir1/file1.txt && + test -f dir1/file2.txt && + test ! -f dir1/untracked.txt && + test -f dir2/file1.txt && + test -f dir2/file2.txt && + test -f dir2/untracked.txt && + test ! -d dir3 && + test ! -f dir3/untracked.txt +' + +test_expect_success 'verify folder entries include all files' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/\0" + EOF + mkdir -p dir1/dir2 && + touch dir1/a && + touch dir1/b && + touch dir1/dir2/a && + touch dir1/dir2/b && + git status -su > actual && + cat > expected <<-\EOF && + ?? dir1/a + ?? dir1/b + ?? dir1/untracked.txt + EOF + test_cmp expected actual +' + +test_expect_success 'verify case insensitivity of virtual file system entries' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/a\0" + printf "Dir1/Dir2/a\0" + printf "DIR2/\0" + EOF + mkdir -p dir1/dir2 && + touch dir1/a && + touch dir1/b && + touch dir1/dir2/a && + touch dir1/dir2/b && + git -c core.ignorecase=false status -su > actual && + cat > expected <<-\EOF && + ?? dir1/a + EOF + test_cmp expected actual && + git -c core.ignorecase=true status -su > actual && + cat > expected <<-\EOF && + ?? dir1/a + ?? dir1/dir2/a + ?? dir2/untracked.txt + EOF + test_cmp expected actual +' + +test_expect_success 'on file created' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file3.txt\0" + EOF + touch dir1/file3.txt && + git add . && + git ls-files -v > actual && + cat > expected <<-\EOF && + S dir1/file1.txt + S dir1/file2.txt + H dir1/file3.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_expect_success 'on file renamed' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + printf "dir1/file3.txt\0" + EOF + mv dir1/file1.txt dir1/file3.txt && + git status -su > actual && + cat > expected <<-\EOF && + D dir1/file1.txt + ?? dir1/file3.txt + EOF + test_cmp expected actual +' + +test_expect_success 'on file deleted' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + EOF + rm dir1/file1.txt && + git status -su > actual && + cat > expected <<-\EOF && + D dir1/file1.txt + EOF + test_cmp expected actual +' + +test_expect_success 'on file overwritten' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/file1.txt\0" + EOF + echo "overwritten" > dir1/file1.txt && + git status -su > actual && + cat > expected <<-\EOF && + M dir1/file1.txt + EOF + test_cmp expected actual +' + +test_expect_success 'on folder created' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/dir1/\0" + EOF + mkdir -p dir1/dir1 && + git status -su > actual && + cat > expected <<-\EOF && + EOF + test_cmp expected actual && + git clean -fd && + test ! -d "/dir1/dir1" +' + +test_expect_success 'on folder renamed' ' + clean_repo && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir3/\0" + printf "dir1/file1.txt\0" + printf "dir1/file2.txt\0" + printf "dir3/file1.txt\0" + printf "dir3/file2.txt\0" + EOF + mv dir1 dir3 && + git status -su > actual && + cat > expected <<-\EOF && + D dir1/file1.txt + D dir1/file2.txt + ?? dir3/file1.txt + ?? dir3/file2.txt + ?? dir3/untracked.txt + EOF + test_cmp expected actual +' + +test_expect_success 'folder with same prefix as file' ' + clean_repo && + touch dir1.sln && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/\0" + printf "dir1.sln\0" + EOF + git add dir1.sln && + git ls-files -v > actual && + cat > expected <<-\EOF && + H dir1.sln + H dir1/file1.txt + H dir1/file2.txt + S dir2/file1.txt + S dir2/file2.txt + EOF + test_cmp expected actual +' + +test_done diff --git a/unpack-trees.c b/unpack-trees.c index 84398d257de278..98f352acca76f3 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -4,6 +4,7 @@ #include "git-compat-util.h" #include "advice.h" #include "gvfs.h" +#include "virtualfilesystem.h" #include "strvec.h" #include "repository.h" #include "parse.h" @@ -1707,6 +1708,14 @@ static int clear_ce_flags_1(struct index_state *istate, continue; } + /* if it's not in the virtual file system, exit early */ + if (core_virtualfilesystem) { + if (is_included_in_virtualfilesystem(ce->name, ce->ce_namelen) > 0) + ce->ce_flags &= ~clear_mask; + cache++; + continue; + } + if (prefix->len && strncmp(ce->name, prefix->buf, prefix->len)) break; @@ -1934,7 +1943,10 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options if (!o->skip_sparse_checkout) { memset(&pl, 0, sizeof(pl)); free_pattern_list = 1; - populate_from_existing_patterns(o, &pl); + if (core_virtualfilesystem) + o->internal.pl = &pl; + else + populate_from_existing_patterns(o, &pl); } index_state_init(&o->internal.result, o->src_index->repo); diff --git a/virtualfilesystem.c b/virtualfilesystem.c new file mode 100644 index 00000000000000..cb040dc97c24f7 --- /dev/null +++ b/virtualfilesystem.c @@ -0,0 +1,312 @@ +#define USE_THE_REPOSITORY_VARIABLE + +#include "git-compat-util.h" +#include "environment.h" +#include "gettext.h" +#include "config.h" +#include "dir.h" +#include "hashmap.h" +#include "run-command.h" +#include "name-hash.h" +#include "read-cache-ll.h" +#include "virtualfilesystem.h" + +#define HOOK_INTERFACE_VERSION (1) + +static struct strbuf virtual_filesystem_data = STRBUF_INIT; +static struct hashmap virtual_filesystem_hashmap; +static struct hashmap parent_directory_hashmap; + +struct virtualfilesystem { + struct hashmap_entry ent; /* must be the first member! */ + const char *pattern; + int patternlen; +}; + +static unsigned int(*vfshash)(const void *buf, size_t len); +static int(*vfscmp)(const char *a, const char *b, size_t len); + +static int vfs_hashmap_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *he1, + const struct hashmap_entry *he2, + const void *key UNUSED) +{ + const struct virtualfilesystem *vfs1 = + container_of(he1, const struct virtualfilesystem, ent); + const struct virtualfilesystem *vfs2 = + container_of(he2, const struct virtualfilesystem, ent); + + return vfscmp(vfs1->pattern, vfs2->pattern, vfs1->patternlen); +} + +static void get_virtual_filesystem_data(struct repository *r, struct strbuf *vfs_data) +{ + struct child_process cp = CHILD_PROCESS_INIT; + int err; + + strbuf_init(vfs_data, 0); + + strvec_push(&cp.args, core_virtualfilesystem); + strvec_pushf(&cp.args, "%d", HOOK_INTERFACE_VERSION); + cp.use_shell = 1; + cp.dir = repo_get_work_tree(r); + + err = capture_command(&cp, vfs_data, 1024); + if (err) + die("unable to load virtual file system"); +} + +static int check_includes_hashmap(struct hashmap *map, const char *pattern, int patternlen) +{ + struct strbuf sb = STRBUF_INIT; + struct virtualfilesystem vfs; + char *slash; + + /* Check straight mapping */ + strbuf_reset(&sb); + strbuf_add(&sb, pattern, patternlen); + vfs.pattern = sb.buf; + vfs.patternlen = sb.len; + hashmap_entry_init(&vfs.ent, vfshash(vfs.pattern, vfs.patternlen)); + if (hashmap_get_entry(map, &vfs, ent, NULL)) { + strbuf_release(&sb); + return 1; + } + + /* + * Check to see if it matches a directory or any path + * underneath it. In other words, 'a/b/foo.txt' will match + * '/', 'a/', and 'a/b/'. + */ + slash = strchr(sb.buf, '/'); + while (slash) { + vfs.pattern = sb.buf; + vfs.patternlen = slash - sb.buf + 1; + hashmap_entry_init(&vfs.ent, vfshash(vfs.pattern, vfs.patternlen)); + if (hashmap_get_entry(map, &vfs, ent, NULL)) { + strbuf_release(&sb); + return 1; + } + slash = strchr(slash + 1, '/'); + } + + strbuf_release(&sb); + return 0; +} + +static void includes_hashmap_add(struct hashmap *map, const char *pattern, const int patternlen) +{ + struct virtualfilesystem *vfs; + + vfs = xmalloc(sizeof(struct virtualfilesystem)); + vfs->pattern = pattern; + vfs->patternlen = patternlen; + hashmap_entry_init(&vfs->ent, vfshash(vfs->pattern, vfs->patternlen)); + hashmap_add(map, &vfs->ent); +} + +static void initialize_includes_hashmap(struct hashmap *map, struct strbuf *vfs_data) +{ + char *buf, *entry; + size_t len, i; + + /* + * Build a hashmap of the virtual file system data we can use to look + * for cache entry matches quickly + */ + vfshash = ignore_case ? memihash : memhash; + vfscmp = ignore_case ? strncasecmp : strncmp; + hashmap_init(map, vfs_hashmap_cmp, NULL, 0); + + entry = buf = vfs_data->buf; + len = vfs_data->len; + for (i = 0; i < len; i++) { + if (buf[i] == '\0') { + includes_hashmap_add(map, entry, buf + i - entry); + entry = buf + i + 1; + } + } +} + +/* + * Return 1 if the requested item is found in the virtual file system, + * 0 for not found and -1 for undecided. + */ +int is_included_in_virtualfilesystem(const char *pathname, int pathlen) +{ + if (!core_virtualfilesystem) + return -1; + + if (!virtual_filesystem_hashmap.tablesize && virtual_filesystem_data.len) + initialize_includes_hashmap(&virtual_filesystem_hashmap, &virtual_filesystem_data); + if (!virtual_filesystem_hashmap.tablesize) + return -1; + + return check_includes_hashmap(&virtual_filesystem_hashmap, pathname, pathlen); +} + +static void parent_directory_hashmap_add(struct hashmap *map, const char *pattern, const int patternlen) +{ + char *slash; + struct virtualfilesystem *vfs; + + /* + * Add any directories leading up to the file as the excludes logic + * needs to match directories leading up to the files as well. Detect + * and prevent unnecessary duplicate entries which will be common. + */ + if (patternlen > 1) { + slash = strchr(pattern + 1, '/'); + while (slash) { + vfs = xmalloc(sizeof(struct virtualfilesystem)); + vfs->pattern = pattern; + vfs->patternlen = slash - pattern + 1; + hashmap_entry_init(&vfs->ent, vfshash(vfs->pattern, vfs->patternlen)); + if (hashmap_get_entry(map, vfs, ent, NULL)) + free(vfs); + else + hashmap_add(map, &vfs->ent); + slash = strchr(slash + 1, '/'); + } + } +} + +static void initialize_parent_directory_hashmap(struct hashmap *map, struct strbuf *vfs_data) +{ + char *buf, *entry; + size_t len, i; + + /* + * Build a hashmap of the parent directories contained in the virtual + * file system data we can use to look for matches quickly + */ + vfshash = ignore_case ? memihash : memhash; + vfscmp = ignore_case ? strncasecmp : strncmp; + hashmap_init(map, vfs_hashmap_cmp, NULL, 0); + + entry = buf = vfs_data->buf; + len = vfs_data->len; + for (i = 0; i < len; i++) { + if (buf[i] == '\0') { + parent_directory_hashmap_add(map, entry, buf + i - entry); + entry = buf + i + 1; + } + } +} + +static int check_directory_hashmap(struct hashmap *map, const char *pathname, int pathlen) +{ + struct strbuf sb = STRBUF_INIT; + struct virtualfilesystem vfs; + + /* Check for directory */ + strbuf_reset(&sb); + strbuf_add(&sb, pathname, pathlen); + strbuf_addch(&sb, '/'); + vfs.pattern = sb.buf; + vfs.patternlen = sb.len; + hashmap_entry_init(&vfs.ent, vfshash(vfs.pattern, vfs.patternlen)); + if (hashmap_get_entry(map, &vfs, ent, NULL)) { + strbuf_release(&sb); + return 0; + } + + strbuf_release(&sb); + return 1; +} + +/* + * Return 1 for exclude, 0 for include and -1 for undecided. + */ +int is_excluded_from_virtualfilesystem(const char *pathname, int pathlen, int dtype) +{ + if (!core_virtualfilesystem) + return -1; + + if (dtype != DT_REG && dtype != DT_DIR && dtype != DT_LNK) + die(_("is_excluded_from_virtualfilesystem passed unhandled dtype")); + + if (dtype == DT_REG || dtype == DT_LNK) { + int ret = is_included_in_virtualfilesystem(pathname, pathlen); + if (ret > 0) + return 0; + if (ret == 0) + return 1; + return ret; + } + + if (dtype == DT_DIR) { + if (!parent_directory_hashmap.tablesize && virtual_filesystem_data.len) + initialize_parent_directory_hashmap(&parent_directory_hashmap, &virtual_filesystem_data); + if (!parent_directory_hashmap.tablesize) + return -1; + + return check_directory_hashmap(&parent_directory_hashmap, pathname, pathlen); + } + + return -1; +} + +/* + * Update the CE_SKIP_WORKTREE bits based on the virtual file system. + */ +void apply_virtualfilesystem(struct index_state *istate) +{ + char *buf, *entry; + size_t i; + + if (!repo_config_get_virtualfilesystem(istate->repo)) + return; + + if (!virtual_filesystem_data.len) + get_virtual_filesystem_data(istate->repo, &virtual_filesystem_data); + + /* set CE_SKIP_WORKTREE bit on all entries */ + for (i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags |= CE_SKIP_WORKTREE; + + /* clear CE_SKIP_WORKTREE bit for everything in the virtual file system */ + entry = buf = virtual_filesystem_data.buf; + for (i = 0; i < virtual_filesystem_data.len; i++) { + if (buf[i] == '\0') { + ssize_t pos, len; + + len = buf + i - entry; + + /* look for a directory wild card (ie "dir1/") */ + if (buf[i - 1] == '/') { + if (ignore_case) + adjust_dirname_case(istate, entry); + pos = index_name_pos(istate, entry, len); + if (pos < 0) { + pos = -pos - 1; + while ((size_t)pos < istate->cache_nr && !fspathncmp(istate->cache[pos]->name, entry, len)) { + istate->cache[pos]->ce_flags &= ~CE_SKIP_WORKTREE; + pos++; + } + } + } else { + if (ignore_case) { + struct cache_entry *ce = index_file_exists(istate, entry, len, ignore_case); + if (ce) + ce->ce_flags &= ~CE_SKIP_WORKTREE; + } else { + int pos = index_name_pos(istate, entry, len); + if (pos >= 0) + istate->cache[pos]->ce_flags &= ~CE_SKIP_WORKTREE; + } + } + + entry += len + 1; + } + } +} + +/* + * Free the virtual file system data structures. + */ +void free_virtualfilesystem(void) { + hashmap_clear_and_free(&virtual_filesystem_hashmap, struct virtualfilesystem, ent); + hashmap_clear_and_free(&parent_directory_hashmap, struct virtualfilesystem, ent); + strbuf_release(&virtual_filesystem_data); +} diff --git a/virtualfilesystem.h b/virtualfilesystem.h new file mode 100644 index 00000000000000..5e8c5b096df09a --- /dev/null +++ b/virtualfilesystem.h @@ -0,0 +1,25 @@ +#ifndef VIRTUALFILESYSTEM_H +#define VIRTUALFILESYSTEM_H + +/* + * Update the CE_SKIP_WORKTREE bits based on the virtual file system. + */ +void apply_virtualfilesystem(struct index_state *istate); + +/* + * Return 1 if the requested item is found in the virtual file system, + * 0 for not found and -1 for undecided. + */ +int is_included_in_virtualfilesystem(const char *pathname, int pathlen); + +/* + * Return 1 for exclude, 0 for include and -1 for undecided. + */ +int is_excluded_from_virtualfilesystem(const char *pathname, int pathlen, int dtype); + +/* + * Free the virtual file system data structures. + */ +void free_virtualfilesystem(void); + +#endif diff --git a/wt-status.c b/wt-status.c index c9e14101424991..00403b74f180ae 100644 --- a/wt-status.c +++ b/wt-status.c @@ -1612,6 +1612,8 @@ static void show_sparse_checkout_in_use(struct wt_status *s, { if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_DISABLED) return; + if (core_virtualfilesystem) + return; if (s->state.sparse_checkout_percentage == SPARSE_CHECKOUT_SPARSE_INDEX) status_printf_ln(s, color, _("You are in a sparse checkout.")); From d04b4997edd9ef43306e7f91013b746ab02605e0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Aug 2025 12:47:31 +0200 Subject: [PATCH 076/229] gvfs: allow corrupt objects to be re-downloaded As of 9e59b38c88c (object-file: emit corruption errors when detected, 2022-12-14), Git will loudly complain about corrupt objects. That is fine, as long as the idea isn't to re-download locally-corrupted objects. But that's exactly what we want to do in VFS for Git via the `read-object` hook, as per the `GitCorruptObjectTests` code added in https://github.com/microsoft/VFSForGit/commit/2db0c030eb25 (New features: [...] - GVFS can now recover from corrupted git object files [...] , 2018-02-16). So let's support precisely that, and add a regression test that ensures that re-downloading corrupt objects via the `read-object` hook works. While at it, avoid the XOR operator to flip the bits, when we actually want to make sure that they are turned off: Use the AND-NOT operator for that purpose. Helped-by: Matthew John Cheetham Helped-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- commit.c | 9 +++++++-- odb.c | 3 +++ t/t1060-object-corruption.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/commit.c b/commit.c index 9dba840a9cd279..37d673f5a1f781 100644 --- a/commit.c +++ b/commit.c @@ -567,9 +567,14 @@ int repo_parse_commit_internal(struct repository *r, int flags = OBJECT_INFO_LOOKUP_REPLACE | OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_DIE_IF_CORRUPT; - /* But the GVFS Protocol _does_ support missing commits! */ + /* + * But the GVFS Protocol _does_ support missing commits! + * And the idea with VFS for Git is to re-download corrupted objects, + * not to fail! + */ if (gvfs_config_is_set(r, GVFS_MISSING_OK)) - flags ^= OBJECT_INFO_SKIP_FETCH_OBJECT; + flags &= ~(OBJECT_INFO_SKIP_FETCH_OBJECT | + OBJECT_INFO_DIE_IF_CORRUPT); if (!item) return -1; diff --git a/odb.c b/odb.c index e8d67d30ec4a95..3afecee47d9e71 100644 --- a/odb.c +++ b/odb.c @@ -1022,6 +1022,9 @@ void *odb_read_object(struct object_database *odb, unsigned flags = OBJECT_INFO_DIE_IF_CORRUPT | OBJECT_INFO_LOOKUP_REPLACE; void *data; + if (gvfs_config_is_set(odb->repo, GVFS_MISSING_OK)) + flags &= ~OBJECT_INFO_DIE_IF_CORRUPT; + oi.typep = type; oi.sizep = size; oi.contentp = &data; diff --git a/t/t1060-object-corruption.sh b/t/t1060-object-corruption.sh index 502a5ea1c51e3a..4f1f593c7410bd 100755 --- a/t/t1060-object-corruption.sh +++ b/t/t1060-object-corruption.sh @@ -3,6 +3,7 @@ test_description='see how we handle various forms of corruption' . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-diff-data.sh # convert "1234abcd" to ".git/objects/12/34abcd" obj_to_file() { @@ -62,6 +63,35 @@ test_expect_success 'streaming a corrupt blob fails' ' ) ' +test_expect_success PERL 'truncated objects can be re-retrieved via GVFS' ' + git init truncated && + COPYING_test_data >truncated/COPYING && + git -C truncated add COPYING && + test_tick && + git -C truncated commit -m initial COPYING && + + # set up the `read-object` hook so that it overwrites the corrupt object + mkdir -p truncated/.git/hooks && + sed -e "1s|/usr/bin/perl|$PERL_PATH|" \ + -e "s/system/unlink \".git\/objects\/\" . substr(\$sha1, 0, 2) . \"\/\" . substr(\$sha1, 2); &/" \ + <$TEST_DIRECTORY/t0410/read-object \ + >truncated/.git/hooks/read-object && + chmod +x truncated/.git/hooks/read-object && + + # ensure that the parent repository has a copy of the object, from + # where the `read-object` can read it + sha="$(git hash-object -w truncated/COPYING)" && + file=$(obj_to_file $sha) && + size=$(test_file_size $file) && + chmod u+w truncated/$file && + test-tool truncate truncated/$file $(($size-8)) && + + rm truncated/COPYING && + test_must_fail git -C truncated reset --hard && + git -C truncated -c core.gvfs=4 -c core.virtualizeObjects \ + reset --hard +' + test_expect_success 'getting type of a corrupt blob fails' ' ( cd bit-error && From c335e49f7ea93d52e88fcf7e264aa017fe3481b6 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 6 Mar 2025 11:14:37 +0000 Subject: [PATCH 077/229] git.c: permit repack cmd in Scalar repos Loosen the blocking of the `repack` command from all "GVFS repos" (those that have `core.gvfs` set) to only those that actually use the virtual file system (VFS for Git only). This allows for `repack` to be used in Scalar clones. Signed-off-by: Matthew John Cheetham --- git.c | 2 +- t/t0402-block-command-on-gvfs.sh | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/git.c b/git.c index 0d1168166ef504..3391f49db9b6b6 100644 --- a/git.c +++ b/git.c @@ -713,7 +713,7 @@ static struct cmd_struct commands[] = { { "remote", cmd_remote, RUN_SETUP }, { "remote-ext", cmd_remote_ext, NO_PARSEOPT }, { "remote-fd", cmd_remote_fd, NO_PARSEOPT }, - { "repack", cmd_repack, RUN_SETUP | BLOCK_ON_GVFS_REPO }, + { "repack", cmd_repack, RUN_SETUP | BLOCK_ON_VFS_ENABLED }, { "replace", cmd_replace, RUN_SETUP }, { "replay", cmd_replay, RUN_SETUP }, { "repo", cmd_repo, RUN_SETUP }, diff --git a/t/t0402-block-command-on-gvfs.sh b/t/t0402-block-command-on-gvfs.sh index 3ec7620ce6194d..a8f7fb07a603e2 100755 --- a/t/t0402-block-command-on-gvfs.sh +++ b/t/t0402-block-command-on-gvfs.sh @@ -22,7 +22,6 @@ not_with_gvfs fsck not_with_gvfs gc not_with_gvfs gc --auto not_with_gvfs prune -not_with_gvfs repack not_with_gvfs submodule status not_with_gvfs update-index --index-version 2 not_with_gvfs update-index --skip-worktree @@ -36,4 +35,14 @@ test_expect_success 'test gc --auto succeeds when disabled via config' ' git gc --auto ' +test_expect_success 'test repack fails with VFS bit enabled' ' + test_config core.gvfs true && + test_must_fail git repack +' + +test_expect_success 'test repack succeeds with VFS bit disabled' ' + test_config core.gvfs 150 && + git repack +' + test_done From b4e1cd0feaf3629747c5f8bff349a341737370ee Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Wed, 1 Aug 2018 13:26:22 -0400 Subject: [PATCH 078/229] virtualfilesystem: don't run the virtual file system hook if the index has been redirected Fixes #13 Some git commands spawn helpers and redirect the index to a different location. These include "difftool -d" and the sequencer (i.e. `git rebase -i`, `git cherry-pick` and `git revert`) and others. In those instances we don't want to update their temporary index with our virtualization data. Helped-by: Johannes Schindelin Signed-off-by: Ben Peart --- config.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index 53ff9958e9c130..629e6379aea000 100644 --- a/config.c +++ b/config.c @@ -2494,11 +2494,25 @@ int repo_config_get_virtualfilesystem(struct repository *r) if (core_virtualfilesystem && !*core_virtualfilesystem) FREE_AND_NULL(core_virtualfilesystem); - /* virtual file system relies on the sparse checkout logic so force it on */ if (core_virtualfilesystem) { - core_apply_sparse_checkout = 1; - virtual_filesystem_result = 1; - return 1; + /* + * Some git commands spawn helpers and redirect the index to a different + * location. These include "difftool -d" and the sequencer + * (i.e. `git rebase -i`, `git cherry-pick` and `git revert`) and others. + * In those instances we don't want to update their temporary index with + * our virtualization data. + */ + char *default_index_file = xstrfmt("%s/%s", r->gitdir, "index"); + int should_run_hook = !strcmp(default_index_file, r->index_file); + + free(default_index_file); + if (should_run_hook) { + /* virtual file system relies on the sparse checkout logic so force it on */ + core_apply_sparse_checkout = 1; + virtual_filesystem_result = 1; + return 1; + } + FREE_AND_NULL(core_virtualfilesystem); } virtual_filesystem_result = 0; From 610bacbe17a56707c359c7f6572f89e281572a1f Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Mon, 3 Jul 2017 13:39:45 -0600 Subject: [PATCH 079/229] cache-tree: remove use of strbuf_addf in update_one String formatting can be a performance issue when there are hundreds of thousands of trees. Change to stop using the strbuf_addf and just add the strings or characters individually. There are a limited number of modes so added a switch for the known ones and a default case if something comes through that are not a known one for git. In one scenario regarding a huge worktree, this reduces the time required for a `git checkout ` from 44 seconds to 38 seconds, i.e. it is a non-negligible performance improvement. Signed-off-by: Kevin Willford --- cache-tree.c | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cache-tree.c b/cache-tree.c index 60416dc3b973ab..ab0b248ab4b0a2 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -437,7 +437,29 @@ static int update_one(struct cache_tree *it, continue; strbuf_grow(&buffer, entlen + 100); - strbuf_addf(&buffer, "%o %.*s%c", mode, entlen, path + baselen, '\0'); + + switch (mode) { + case 0100644: + strbuf_add(&buffer, "100644 ", 7); + break; + case 0100664: + strbuf_add(&buffer, "100664 ", 7); + break; + case 0100755: + strbuf_add(&buffer, "100755 ", 7); + break; + case 0120000: + strbuf_add(&buffer, "120000 ", 7); + break; + case 0160000: + strbuf_add(&buffer, "160000 ", 7); + break; + default: + strbuf_addf(&buffer, "%o ", mode); + break; + } + strbuf_add(&buffer, path + baselen, entlen); + strbuf_addch(&buffer, '\0'); strbuf_add(&buffer, oid->hash, the_hash_algo->rawsz); #if DEBUG_CACHE_TREE From fc23cca8f3ca76a3f1fb98543088572c7b487611 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Mar 2025 13:31:14 +0000 Subject: [PATCH 080/229] git.c: permit fsck cmd in Scalar repos Loosen the blocking of the `fsck` command from all "GVFS repos" (those that have `core.gvfs` set) to only those that actually use the virtual file system (VFS for Git only). This allows for `fsck` to be used in Scalar clones. Signed-off-by: Matthew John Cheetham --- git.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.c b/git.c index 3391f49db9b6b6..9f1aa4775a19b3 100644 --- a/git.c +++ b/git.c @@ -656,7 +656,7 @@ static struct cmd_struct commands[] = { { "for-each-ref", cmd_for_each_ref, RUN_SETUP }, { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY }, { "format-patch", cmd_format_patch, RUN_SETUP }, - { "fsck", cmd_fsck, RUN_SETUP | BLOCK_ON_GVFS_REPO}, + { "fsck", cmd_fsck, RUN_SETUP | BLOCK_ON_VFS_ENABLED }, { "fsck-objects", cmd_fsck, RUN_SETUP }, { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP }, { "gc", cmd_gc, RUN_SETUP }, From 8b4db2cff9cfa91223e76cd6c35ae91cbb0849f8 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Tue, 9 Oct 2018 10:19:14 -0600 Subject: [PATCH 081/229] virtualfilesystem: check if directory is included Add check to see if a directory is included in the virtualfilesystem before checking the directory hashmap. This allows a directory entry like foo/ to find all untracked files in subdirectories. --- t/t1093-virtualfilesystem.sh | 2 ++ virtualfilesystem.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh index bd0c9f72ba3c4a..8ba9a2a75e093a 100755 --- a/t/t1093-virtualfilesystem.sh +++ b/t/t1093-virtualfilesystem.sh @@ -222,6 +222,8 @@ test_expect_success 'verify folder entries include all files' ' cat > expected <<-\EOF && ?? dir1/a ?? dir1/b + ?? dir1/dir2/a + ?? dir1/dir2/b ?? dir1/untracked.txt EOF test_cmp expected actual diff --git a/virtualfilesystem.c b/virtualfilesystem.c index cb040dc97c24f7..3f00800d86ef47 100644 --- a/virtualfilesystem.c +++ b/virtualfilesystem.c @@ -236,6 +236,10 @@ int is_excluded_from_virtualfilesystem(const char *pathname, int pathlen, int dt } if (dtype == DT_DIR) { + int ret = is_included_in_virtualfilesystem(pathname, pathlen); + if (ret > 0) + return 0; + if (!parent_directory_hashmap.tablesize && virtual_filesystem_data.len) initialize_parent_directory_hashmap(&parent_directory_hashmap, &virtual_filesystem_data); if (!parent_directory_hashmap.tablesize) From b88a9ce8987c8e29e2b2c2b384a874b5f32b24ea Mon Sep 17 00:00:00 2001 From: Ben Peart Date: Thu, 6 Dec 2018 11:09:19 -0500 Subject: [PATCH 082/229] gvfs: block unsupported commands when running in a GVFS repo The following commands and options are not currently supported when working in a GVFS repo. Add code to detect and block these commands from executing. 1) fsck 2) gc 4) prune 5) repack 6) submodule 8) update-index --split-index 9) update-index --index-version (other than 4) 10) update-index --[no-]skip-worktree 11) worktree Signed-off-by: Ben Peart Signed-off-by: Johannes Schindelin --- builtin/gc.c | 7 +++++- builtin/update-index.c | 12 +++++++++- git.c | 15 ++++++++---- gvfs.h | 1 + t/meson.build | 1 + t/t0402-block-command-on-gvfs.sh | 39 ++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 7 deletions(-) create mode 100755 t/t0402-block-command-on-gvfs.sh diff --git a/builtin/gc.c b/builtin/gc.c index d212cbb9b84781..6719c1f27bae3c 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -19,6 +19,7 @@ #include "dir.h" #include "environment.h" #include "hex.h" +#include "gvfs.h" #include "config.h" #include "tempfile.h" #include "lockfile.h" @@ -843,7 +844,7 @@ static int gc_foreground_tasks(struct maintenance_run_opts *opts, int cmd_gc(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { int aggressive = 0; int force = 0; @@ -925,6 +926,10 @@ int cmd_gc(int argc, if (opts.quiet) strvec_push(&repack_args, "-q"); + if ((!opts.auto_flag || (opts.auto_flag && cfg.gc_auto_threshold > 0)) && + gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) + die(_("'git gc' is not supported on a GVFS repo")); + if (opts.auto_flag) { if (cfg.detach_auto && opts.detach < 0) opts.detach = 1; diff --git a/builtin/update-index.c b/builtin/update-index.c index 8a5907767bf297..96cc15e9b713dd 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -8,6 +8,7 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "gvfs.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -914,7 +915,7 @@ static enum parse_opt_result reupdate_callback( int cmd_update_index(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { int newfd, entries, has_errors = 0, nul_term_line = 0; enum uc_mode untracked_cache = UC_UNSPECIFIED; @@ -1178,7 +1179,13 @@ int cmd_update_index(int argc, argc = parse_options_end(&ctx); getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + if (mark_skip_worktree_only && gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) + die(_("modifying the skip worktree bit is not supported on a GVFS repo")); + if (preferred_index_format) { + if (preferred_index_format != 4 && gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) + die(_("changing the index version is not supported on a GVFS repo")); + if (preferred_index_format < 0) { printf(_("%d\n"), the_repository->index->version); } else if (preferred_index_format < INDEX_FORMAT_LB || @@ -1224,6 +1231,9 @@ int cmd_update_index(int argc, odb_transaction_commit(transaction); if (split_index > 0) { + if (gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) + die(_("split index is not supported on a GVFS repo")); + if (repo_config_get_split_index(the_repository) == 0) warning(_("core.splitIndex is set to false; " "remove or change it, if you really want to " diff --git a/git.c b/git.c index a52b94a78b3c2a..252a4d6d358dcb 100644 --- a/git.c +++ b/git.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "gvfs.h" #include "config.h" #include "environment.h" #include "exec-cmd.h" @@ -31,6 +32,7 @@ #define DELAY_PAGER_CONFIG (1<<4) #define NO_PARSEOPT (1<<5) /* parse-options is not used */ #define DEPRECATED (1<<6) +#define BLOCK_ON_GVFS_REPO (1<<7) /* command not allowed in GVFS repos */ struct cmd_struct { const char *cmd; @@ -563,6 +565,9 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct if (!help && p->option & NEED_WORK_TREE) setup_work_tree(); + if (!help && p->option & BLOCK_ON_GVFS_REPO && gvfs_config_is_set(repo, GVFS_BLOCK_COMMANDS)) + die("'git %s' is not supported on a GVFS repo", p->cmd); + if (run_pre_command_hook(the_repository, argv)) die("pre-command hook aborted command"); @@ -647,7 +652,7 @@ static struct cmd_struct commands[] = { { "for-each-ref", cmd_for_each_ref, RUN_SETUP }, { "for-each-repo", cmd_for_each_repo, RUN_SETUP_GENTLY }, { "format-patch", cmd_format_patch, RUN_SETUP }, - { "fsck", cmd_fsck, RUN_SETUP }, + { "fsck", cmd_fsck, RUN_SETUP | BLOCK_ON_GVFS_REPO}, { "fsck-objects", cmd_fsck, RUN_SETUP }, { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP }, { "gc", cmd_gc, RUN_SETUP }, @@ -691,7 +696,7 @@ static struct cmd_struct commands[] = { { "pack-refs", cmd_pack_refs, RUN_SETUP }, { "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "pickaxe", cmd_blame, RUN_SETUP }, - { "prune", cmd_prune, RUN_SETUP }, + { "prune", cmd_prune, RUN_SETUP | BLOCK_ON_GVFS_REPO}, { "prune-packed", cmd_prune_packed, RUN_SETUP }, { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE }, { "push", cmd_push, RUN_SETUP }, @@ -704,7 +709,7 @@ static struct cmd_struct commands[] = { { "remote", cmd_remote, RUN_SETUP }, { "remote-ext", cmd_remote_ext, NO_PARSEOPT }, { "remote-fd", cmd_remote_fd, NO_PARSEOPT }, - { "repack", cmd_repack, RUN_SETUP }, + { "repack", cmd_repack, RUN_SETUP | BLOCK_ON_GVFS_REPO }, { "replace", cmd_replace, RUN_SETUP }, { "replay", cmd_replay, RUN_SETUP }, { "repo", cmd_repo, RUN_SETUP }, @@ -726,7 +731,7 @@ static struct cmd_struct commands[] = { { "stash", cmd_stash, RUN_SETUP | NEED_WORK_TREE }, { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE }, { "stripspace", cmd_stripspace }, - { "submodule--helper", cmd_submodule__helper, RUN_SETUP }, + { "submodule--helper", cmd_submodule__helper, RUN_SETUP | BLOCK_ON_GVFS_REPO }, { "survey", cmd_survey, RUN_SETUP }, { "switch", cmd_switch, RUN_SETUP | NEED_WORK_TREE }, { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, @@ -747,7 +752,7 @@ static struct cmd_struct commands[] = { #ifndef WITH_BREAKING_CHANGES { "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED }, #endif - { "worktree", cmd_worktree, RUN_SETUP }, + { "worktree", cmd_worktree, RUN_SETUP | BLOCK_ON_GVFS_REPO }, { "write-tree", cmd_write_tree, RUN_SETUP }, }; diff --git a/gvfs.h b/gvfs.h index 59ee3893b1efa6..8368f950243c63 100644 --- a/gvfs.h +++ b/gvfs.h @@ -12,6 +12,7 @@ struct repository; * The list of bits in the core_gvfs setting */ #define GVFS_SKIP_SHA_ON_INDEX (1 << 0) +#define GVFS_BLOCK_COMMANDS (1 << 1) #define GVFS_MISSING_OK (1 << 2) /* diff --git a/t/meson.build b/t/meson.build index 95c4a0bbf0f380..d53f4aa25ff576 100644 --- a/t/meson.build +++ b/t/meson.build @@ -138,6 +138,7 @@ integration_tests = [ 't0303-credential-external.sh', 't0400-pre-command-hook.sh', 't0401-post-command-hook.sh', + 't0402-block-command-on-gvfs.sh', 't0410-partial-clone.sh', 't0411-clone-from-partial.sh', 't0450-txt-doc-vs-help.sh', diff --git a/t/t0402-block-command-on-gvfs.sh b/t/t0402-block-command-on-gvfs.sh new file mode 100755 index 00000000000000..3ec7620ce6194d --- /dev/null +++ b/t/t0402-block-command-on-gvfs.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +test_description='block commands in GVFS repo' + +. ./test-lib.sh + +not_with_gvfs () { + command=$1 && + shift && + test_expect_success "test $command $*" " + test_config alias.g4rbled $command && + test_config core.gvfs true && + test_must_fail git $command $* && + test_must_fail git g4rbled $* && + test_unconfig core.gvfs && + test_must_fail git -c core.gvfs=true $command $* && + test_must_fail git -c core.gvfs=true g4rbled $* + " +} + +not_with_gvfs fsck +not_with_gvfs gc +not_with_gvfs gc --auto +not_with_gvfs prune +not_with_gvfs repack +not_with_gvfs submodule status +not_with_gvfs update-index --index-version 2 +not_with_gvfs update-index --skip-worktree +not_with_gvfs update-index --no-skip-worktree +not_with_gvfs update-index --split-index +not_with_gvfs worktree list + +test_expect_success 'test gc --auto succeeds when disabled via config' ' + test_config core.gvfs true && + test_config gc.auto 0 && + git gc --auto +' + +test_done From 0f1c8faecc53eb046de6b2733040d277d6b1fa56 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 13 Mar 2025 13:31:50 +0000 Subject: [PATCH 083/229] git.c: permit prune cmd in Scalar repos Loosen the blocking of the `prune` command from all "GVFS repos" (those that have `core.gvfs` set) to only those that actually use the virtual file system (VFS for Git only). This allows for `prune` to be used in Scalar clones. Signed-off-by: Matthew John Cheetham --- git.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.c b/git.c index 9f1aa4775a19b3..35aa90a56bce29 100644 --- a/git.c +++ b/git.c @@ -700,7 +700,7 @@ static struct cmd_struct commands[] = { { "pack-refs", cmd_pack_refs, RUN_SETUP }, { "patch-id", cmd_patch_id, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "pickaxe", cmd_blame, RUN_SETUP }, - { "prune", cmd_prune, RUN_SETUP | BLOCK_ON_GVFS_REPO}, + { "prune", cmd_prune, RUN_SETUP | BLOCK_ON_VFS_ENABLED }, { "prune-packed", cmd_prune_packed, RUN_SETUP }, { "pull", cmd_pull, RUN_SETUP | NEED_WORK_TREE }, { "push", cmd_push, RUN_SETUP }, From 879187ee51a062daa5df445e136147882e0717ba Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 28 May 2019 21:48:08 +0200 Subject: [PATCH 084/229] backwards-compatibility: support the post-indexchanged hook When our patches to support that hook were upstreamed, the hook's name was eliciting some reviewer suggestions, and it was renamed to `post-index-change`. These patches (with the new name) made it into v2.22.0. However, VFSforGit users may very well have checkouts with that hook installed under the original name. To support this, let's just introduce a hack where we look a bit more closely when we just failed to find the `post-index-change` hook, and allow any `post-indexchanged` hook to run instead (if it exists). --- hook.c | 14 +++++++++++++- t/t7113-post-index-change-hook.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/hook.c b/hook.c index be04f1a6be4142..072fca9df74a5b 100644 --- a/hook.c +++ b/hook.c @@ -185,7 +185,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name, .hook_name = hook_name, .options = options, }; - const char *const hook_path = find_hook(r, hook_name); + const char *hook_path = find_hook(r, hook_name); int ret = 0; const struct run_process_parallel_opts opts = { .tr2_category = "hook", @@ -201,6 +201,18 @@ int run_hooks_opt(struct repository *r, const char *hook_name, .data = &cb_data, }; + /* + * Backwards compatibility hack in VFS for Git: when originally + * introduced (and used!), it was called `post-indexchanged`, but this + * name was changed during the review on the Git mailing list. + * + * Therefore, when the `post-index-change` hook is not found, let's + * look for a hook with the old name (which would be found in case of + * already-existing checkouts). + */ + if (!hook_path && !strcmp(hook_name, "post-index-change")) + hook_path = find_hook(r, "post-indexchanged"); + if (!options) BUG("a struct run_hooks_opt must be provided to run_hooks"); diff --git a/t/t7113-post-index-change-hook.sh b/t/t7113-post-index-change-hook.sh index c10d94fe3d3b01..22b81a67cc32b6 100755 --- a/t/t7113-post-index-change-hook.sh +++ b/t/t7113-post-index-change-hook.sh @@ -15,6 +15,36 @@ test_expect_success 'setup' ' git commit -m "initial" ' +test_expect_success 'post-indexchanged' ' + mkdir -p .git/hooks && + test_when_finished "rm -f .git/hooks/post-indexchanged marker" && + write_script .git/hooks/post-indexchanged <<-\EOF && + : >marker + EOF + + : make sure -changed is called if -change does not exist && + test_when_finished "echo testing >dir1/file2.txt && git status" && + echo changed >dir1/file2.txt && + : force index to be dirty && + test-tool chmtime -60 .git/index && + git status && + test_path_is_file marker && + + test_when_finished "rm -f .git/hooks/post-index-change marker2" && + write_script .git/hooks/post-index-change <<-\EOF && + : >marker2 + EOF + + : make sure -changed is not called if -change exists && + rm -f marker marker2 && + echo testing >dir1/file2.txt && + : force index to be dirty && + test-tool chmtime -60 .git/index && + git status && + test_path_is_missing marker && + test_path_is_file marker2 +' + test_expect_success 'test status, add, commit, others trigger hook without flags set' ' test_hook post-index-change <<-\EOF && if test "$1" -eq 1; then From 7c289999fd59ebf716810d79b6abc0d2ff6eca42 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Wed, 15 Apr 2020 16:19:31 +0000 Subject: [PATCH 085/229] gvfs: allow overriding core.gvfs In earlier versions of `microsoft/git`, we found a user who had set `core.gvfs = false` in their global config. This should not have been necessary, but it also should not have caused a problem. However, it did. The reason was that `gvfs_load_config_value()` was called from `config.c` when reading config key/value pairs from all the config files. The local config should override the global config, and this is done by `config.c` reading the global config first then reading the local config. However, our logic only allowed writing the `core_gvfs` variable once. In v2.51.0, we had to adapt to upstream changes that changed way the `core.gvfs` config value is read, and the special handling is no longer necessary, yet we still want the test case that ensures that this bug does not experience a regression. Signed-off-by: Derrick Stolee Signed-off-by: Johannes Schindelin --- t/t0021-conversion.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index 2b97db87b0f96e..a9c3dc02f4e55d 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -348,6 +348,10 @@ test_expect_success "filter: smudge filters blocked when under GVFS" ' test_config filter.empty-in-repo.smudge "echo smudged && cat" && test_config core.gvfs 64 && + test_must_fail git checkout && + + # ensure the local core.gvfs setting overwrites the global setting + git config --global core.gvfs false && test_must_fail git checkout ' From 414949f3cca6ee0a6f503bf82c85a8c202cf2871 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 6 Mar 2025 11:16:04 +0000 Subject: [PATCH 086/229] worktree: remove special case GVFS cmd blocking Replace the special casing of the `worktree` command being blocked on VFS-enabled repos with the new `BLOCK_ON_VFS_ENABLED` flag. Signed-off-by: Matthew John Cheetham --- builtin/worktree.c | 7 ------- git.c | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index 6877363a40ad14..f6c246dc078ca2 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -1451,13 +1451,6 @@ int cmd_worktree(int ac, repo_config(the_repository, git_worktree_config, NULL); - /* - * git-worktree is special-cased to work in Scalar repositories - * even when they use the GVFS Protocol. - */ - if (gvfs_config_is_set(repo, GVFS_USE_VIRTUAL_FILESYSTEM)) - die("'git %s' is not supported on a GVFS repo", "worktree"); - if (!prefix) prefix = ""; diff --git a/git.c b/git.c index 35aa90a56bce29..e2b185b89987c1 100644 --- a/git.c +++ b/git.c @@ -756,7 +756,7 @@ static struct cmd_struct commands[] = { #ifndef WITH_BREAKING_CHANGES { "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED }, #endif - { "worktree", cmd_worktree, RUN_SETUP | BLOCK_ON_GVFS_REPO }, + { "worktree", cmd_worktree, RUN_SETUP | BLOCK_ON_VFS_ENABLED }, { "write-tree", cmd_write_tree, RUN_SETUP }, }; From 77cf91b68546a68207a46e45a46fbd23724c0af5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 18 Jun 2021 14:45:20 +0200 Subject: [PATCH 087/229] gvfs: verify that the built-in FSMonitor is disabled When using a virtual file system layer, the FSMonitor does not make sense. Signed-off-by: Johannes Schindelin --- t/t1093-virtualfilesystem.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t1093-virtualfilesystem.sh b/t/t1093-virtualfilesystem.sh index 8ba9a2a75e093a..cad13d680cb199 100755 --- a/t/t1093-virtualfilesystem.sh +++ b/t/t1093-virtualfilesystem.sh @@ -368,4 +368,15 @@ test_expect_success 'folder with same prefix as file' ' test_cmp expected actual ' +test_expect_success MINGW,FSMONITOR_DAEMON 'virtualfilesystem hook disables built-in FSMonitor' ' + clean_repo && + test_config core.usebuiltinfsmonitor true && + write_script .git/hooks/virtualfilesystem <<-\EOF && + printf "dir1/\0" + EOF + git config core.virtualfilesystem .git/hooks/virtualfilesystem && + git status && + test_must_fail git fsmonitor--daemon status +' + test_done From b43844d46c15a31731408d4c06b58f00c20a831b Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Fri, 27 Jul 2018 12:00:44 -0600 Subject: [PATCH 088/229] BRANCHES.md: Add explanation of branches and using forks --- BRANCHES.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 BRANCHES.md diff --git a/BRANCHES.md b/BRANCHES.md new file mode 100644 index 00000000000000..364158375e7d55 --- /dev/null +++ b/BRANCHES.md @@ -0,0 +1,59 @@ +Branches used in this repo +========================== + +The document explains the branching structure that we are using in the VFSForGit repository as well as the forking strategy that we have adopted for contributing. + +Repo Branches +------------- + +1. `vfs-#` + + These branches are used to track the specific version that match Git for Windows with the VFSForGit specific patches on top. When a new version of Git for Windows is released, the VFSForGit patches will be rebased on that windows version and a new gvfs-# branch created to create pull requests against. + + #### Examples + + ``` + vfs-2.27.0 + vfs-2.30.0 + ``` + + The versions of git for VFSForGit are based on the Git for Windows versions. v2.20.0.vfs.1 will correspond with the v2.20.0.windows.1 with the VFSForGit specific patches applied to the windows version. + +2. `vfs-#-exp` + + These branches are for releasing experimental features to early adopters. They + should contain everything within the corresponding `vfs-#` branch; if the base + branch updates, then merge into the `vfs-#-exp` branch as well. + +Tags +---- + +We are using annotated tags to build the version number for git. The build will look back through the commit history to find the first tag matching `v[0-9]*vfs*` and build the git version number using that tag. + +Full releases are of the form `v2.XX.Y.vfs.Z.W` where `v2.XX.Y` comes from the +upstream version and `Z.W` are custom updates within our fork. Specifically, +the `.Z` value represents the "compatibility level" with VFS for Git. Only +increase this version when making a breaking change with a released version +of VFS for Git. The `.W` version is used for minor updates between major +versions. + +Experimental releases are of the form `v2.XX.Y.vfs.Z.W.exp`. The `.exp` +suffix indicates that experimental features are available. The rest of the +version string comes from the full release tag. These versions will only +be made available as pre-releases on the releases page, never a full release. + +Forking +------- + +A personal fork of this repository and a branch in that repository should be used for development. + +These branches should be based on the latest vfs-# branch. If there are work in progress pull requests that you have based on a previous version branch when a new version branch is created, you will need to move your patches to the new branch to get them in that latest version. + +#### Example + +``` +git clone +git remote add ms https://github.com/Microsoft/git.git +git checkout -b my-changes ms/vfs-2.20.0 --no-track +git push -fu origin HEAD +``` From 22e962ed19916af8ef62ce4d48cb36ede64a03d7 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 26 Mar 2025 10:31:38 +0000 Subject: [PATCH 089/229] builtin/repack.c: emit warning when shared cache is present Emit a warning message when the `gvfs.sharedCache` option is set that the `repack` command will not perform repacking on the shared cache. In the future we can teach `repack` to operate on the shared cache, at which point we can drop this commit. Signed-off-by: Matthew John Cheetham --- builtin/repack.c | 6 ++++++ gvfs.h | 2 ++ 2 files changed, 8 insertions(+) diff --git a/builtin/repack.c b/builtin/repack.c index cfdb4c0920b191..04ba7acada6817 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -15,6 +15,7 @@ #include "promisor-remote.h" #include "repack.h" #include "shallow.h" +#include "gvfs.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -110,6 +111,7 @@ int cmd_repack(int argc, struct tempfile *refs_snapshot = NULL; int i, ret; int show_progress; + const char *tmp_obj_dir = NULL; /* variables to be filled by option parsing */ struct repack_config_ctx config_ctx; @@ -244,6 +246,10 @@ int cmd_repack(int argc, write_bitmaps = 0; } + if (gvfs_config_is_set(repo, GVFS_ANY_MASK) && + !repo_config_get_value(repo, "gvfs.sharedcache", &tmp_obj_dir)) + warning(_("shared object cache is configured but will not be repacked")); + if (write_midx && write_bitmaps) { struct strbuf path = STRBUF_INIT; diff --git a/gvfs.h b/gvfs.h index 8368f950243c63..5d3159425c742e 100644 --- a/gvfs.h +++ b/gvfs.h @@ -29,6 +29,8 @@ struct repository; #define GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK (1 << 4) #define GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS (1 << 6) +#define GVFS_ANY_MASK 0xFFFFFFFF + int gvfs_config_is_set(struct repository *r, int mask); int gvfs_virtualize_objects(struct repository *r); From b2d83928d21c45157efbdb639255046ade3223e4 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 21 Jun 2024 17:18:59 -0400 Subject: [PATCH 090/229] wt-status: add trace2 data for sparse-checkout percentage When sparse-checkout is enabled, add the sparse-checkout percentage to the Trace2 data stream. This number was already computed and printed on the console in the "You are in a sparse checkout..." message. It would be helpful to log it too for performance monitoring. Signed-off-by: Jeff Hostetler --- wt-status.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/wt-status.c b/wt-status.c index 00403b74f180ae..9302d8253f3989 100644 --- a/wt-status.c +++ b/wt-status.c @@ -2571,6 +2571,36 @@ void wt_status_print(struct wt_status *s) s->untracked.nr); trace2_data_intmax("status", s->repo, "count/ignored", s->ignored.nr); + switch (s->state.sparse_checkout_percentage) { + case SPARSE_CHECKOUT_DISABLED: + break; + case SPARSE_CHECKOUT_SPARSE_INDEX: + /* + * Log just the observed size of the sparse-index. + * + * When sparse-index is enabled we can have + * sparse-directory entries in addition to individual + * sparse-file entries, so we don't know the complete + * size of the index. And we do not want to force + * expand it just to emit some telemetry data. So we + * cannot report a percentage for the space savings. + * + * It is possible that if the telemetry data is + * aggregated, someone will have a good estimate for + * the size of a fully populated index and can compute + * a percentage after the fact. + */ + trace2_data_intmax("status", s->repo, + "sparse-index/size", + s->repo->index->cache_nr); + break; + default: + trace2_data_intmax("status", s->repo, + "sparse-checkout/percentage", + s->state.sparse_checkout_percentage); + break; + } + trace2_region_enter("status", "print", s->repo); switch (s->status_format) { From d9fcb0b7f79ff07bb6a5d2b0504388ec79a8d9d7 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 22 Aug 2017 11:54:23 -0400 Subject: [PATCH 091/229] status: add status serialization mechanism Teach STATUS to optionally serialize the results of a status computation to a file. Teach STATUS to optionally read an existing serialization file and simply print the results, rather than actually scanning. This is intended for immediate status results on extremely large repos and assumes the use of a service/daemon to maintain a fresh current status snapshot. 2021-10-30: packet_read() changed its prototype in ec9a37d (pkt-line.[ch]: remove unused packet_read_line_buf(), 2021-10-14). 2021-10-30: sscanf() now does an extra check that "%d" goes into an "int" and complains about "uint32_t". Replacing with "%u" fixes the compile-time error. 2021-10-30: string_list_init() was removed by abf897b (string-list.[ch]: remove string_list_init() compatibility function, 2021-09-28), so we need to initialize manually. Signed-off-by: Jeff Hostetler Signed-off-by: Derrick Stolee --- Documentation/config/status.adoc | 6 + Documentation/git-status.adoc | 33 + .../technical/status-serialization-format.txt | 107 +++ Makefile | 2 + builtin/commit.c | 123 +++- contrib/completion/git-completion.bash | 2 +- meson.build | 2 + pkt-line.c | 2 +- pkt-line.h | 1 + t/meson.build | 2 + t/t7522-serialized-status.sh | 141 ++++ t/t7523-status-complete-untracked.sh | 39 ++ wt-status-deserialize.c | 622 ++++++++++++++++++ wt-status-serialize.c | 219 ++++++ wt-status.c | 6 + wt-status.h | 53 +- 16 files changed, 1356 insertions(+), 4 deletions(-) create mode 100644 Documentation/technical/status-serialization-format.txt create mode 100755 t/t7522-serialized-status.sh create mode 100755 t/t7523-status-complete-untracked.sh create mode 100644 wt-status-deserialize.c create mode 100644 wt-status-serialize.c diff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc index 8caf90f51c19a3..7302b066644e73 100644 --- a/Documentation/config/status.adoc +++ b/Documentation/config/status.adoc @@ -77,3 +77,9 @@ status.submoduleSummary:: the --ignore-submodules=dirty command-line option or the 'git submodule summary' command, which shows a similar output but does not honor these settings. + +status.deserializePath:: + EXPERIMENTAL, Pathname to a file containing cached status results + generated by `--serialize`. This will be overridden by + `--deserialize=` on the command line. If the cache file is + invalid or stale, git will fall-back and compute status normally. diff --git a/Documentation/git-status.adoc b/Documentation/git-status.adoc index 9a376886a5867a..fedf86d32718eb 100644 --- a/Documentation/git-status.adoc +++ b/Documentation/git-status.adoc @@ -151,6 +151,19 @@ ignored, then the directory is not shown, but all contents are shown. threshold. See also linkgit:git-diff[1] `--find-renames`. +--serialize[=]:: + (EXPERIMENTAL) Serialize raw status results to stdout in a + format suitable for use by `--deserialize`. Valid values for + `` are "1" and "v1". + +--deserialize[=]:: + (EXPERIMENTAL) Deserialize raw status results from a file or + stdin rather than scanning the worktree. If `` is omitted + and `status.deserializePath` is unset, input is read from stdin. +--no-deserialize:: + (EXPERIMENTAL) Disable implicit deserialization of status results + from the value of `status.deserializePath`. + ...:: See the 'pathspec' entry in linkgit:gitglossary[7]. @@ -424,6 +437,26 @@ quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). +SERIALIZATION and DESERIALIZATION (EXPERIMENTAL) +------------------------------------------------ + +The `--serialize` option allows git to cache the result of a +possibly time-consuming status scan to a binary file. A local +service/daemon watching file system events could use this to +periodically pre-compute a fresh status result. + +Interactive users could then use `--deserialize` to simply +(and immediately) print the last-known-good result without +waiting for the status scan. + +The binary serialization file format includes some worktree state +information allowing `--deserialize` to reject the cached data +and force a normal status scan if, for example, the commit, branch, +or status modes/options change. The format cannot, however, indicate +when the cached data is otherwise stale -- that coordination belongs +to the task driving the serializations. + + CONFIGURATION ------------- diff --git a/Documentation/technical/status-serialization-format.txt b/Documentation/technical/status-serialization-format.txt new file mode 100644 index 00000000000000..475ae814495581 --- /dev/null +++ b/Documentation/technical/status-serialization-format.txt @@ -0,0 +1,107 @@ +Git status serialization format +=============================== + +Git status serialization enables git to dump the results of a status scan +to a binary file. This file can then be loaded by later status invocations +to print the cached status results. + +The file contains the essential fields from: +() the index +() the "struct wt_status" for the overall results +() the contents of "struct wt_status_change_data" for tracked changed files +() the list of untracked and ignored files + +Version 1 Format: +================= + +The V1 file begins with a required header section followed by optional +sections for each type of item (changed, untracked, ignored). Individual +item sections are only present if necessary. Each item section begins +with an item-type header with the number of items in the section. + +Each "line" in the format is encoded using pkt-line with a final LF. +Flush packets are used to terminate sections. + +----------------- +PKT-LINE("version" SP "1") + +[] +[] +[] +----------------- + + +V1 Header +--------- + +The v1-header-section fields are taken directly from "struct wt_status". +Each field is printed on a separate pkt-line. Lines for NULL string +values are omitted. All integers are printed with "%d". OIDs are +printed in hex. + +v1-header-section = + + PKT-LINE() + +v1-index-headers = PKT-LINE("index_mtime" SP SP LF) + +v1-wt-status-headers = PKT-LINE("is_initial" SP LF) + [ PKT-LINE("branch" SP LF) ] + [ PKT-LINE("reference" SP LF) ] + PKT-LINE("show_ignored_files" SP LF) + PKT-LINE("show_untracked_files" SP LF) + PKT-LINE("show_ignored_directory" SP LF) + [ PKT-LINE("ignore_submodule_arg" SP LF) ] + PKT-LINE("detect_rename" SP LF) + PKT-LINE("rename_score" SP LF) + PKT-LINE("rename_limit" SP LF) + PKT-LINE("detect_break" SP LF) + PKT-LINE("sha1_commit" SP LF) + PKT-LINE("committable" SP LF) + PKT-LINE("workdir_dirty" SP LF) + + +V1 Changed Items +---------------- + +The v1-changed-item-section lists all of the changed items with one +item per pkt-line. Each pkt-line contains: a binary block of data +from "struct wt_status_serialize_data_fixed" in a fixed header where +integers are in network byte order and OIDs are in raw (non-hex) form. +This is followed by one or two raw pathnames (not c-quoted) with NUL +terminators (both NULs are always present even if there is no rename). + +v1-changed-item-section = PKT-LINE("changed" SP LF) + [ PKT-LINE( LF) ]+ + PKT-LINE() + +changed_item = + + + + + + + + + + + + NUL + [ ] + NUL + + +V1 Untracked and Ignored Items +------------------------------ + +These sections are simple lists of pathnames. They ARE NOT +c-quoted. + +v1-untracked-item-section = PKT-LINE("untracked" SP LF) + [ PKT-LINE( LF) ]+ + PKT-LINE() + +v1-ignored-item-section = PKT-LINE("ignored" SP LF) + [ PKT-LINE( LF) ]+ + PKT-LINE() diff --git a/Makefile b/Makefile index 4c25827c184b86..b99030489c4be8 100644 --- a/Makefile +++ b/Makefile @@ -1353,6 +1353,8 @@ LIB_OBJS += wrapper.o LIB_OBJS += write-or-die.o LIB_OBJS += ws.o LIB_OBJS += wt-status.o +LIB_OBJS += wt-status-deserialize.o +LIB_OBJS += wt-status-serialize.o LIB_OBJS += xdiff-interface.o LIB_OBJS += xdiff/xdiffi.o LIB_OBJS += xdiff/xemit.o diff --git a/builtin/commit.c b/builtin/commit.c index 4f77abc43294f9..24bc6a5ffa2099 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -166,6 +166,70 @@ static int opt_parse_porcelain(const struct option *opt, const char *arg, int un return 0; } +static int do_serialize = 0; +static int do_implicit_deserialize = 0; +static int do_explicit_deserialize = 0; +static char *deserialize_path = NULL; + +/* + * --serialize | --serialize=1 | --serialize=v1 + * + * Request that we serialize our output rather than printing in + * any of the established formats. Optionally specify serialization + * version. + */ +static int opt_parse_serialize(const struct option *opt, const char *arg, int unset) +{ + enum wt_status_format *value = (enum wt_status_format *)opt->value; + if (unset || !arg) + *value = STATUS_FORMAT_SERIALIZE_V1; + else if (!strcmp(arg, "v1") || !strcmp(arg, "1")) + *value = STATUS_FORMAT_SERIALIZE_V1; + else + die("unsupported serialize version '%s'", arg); + + if (do_explicit_deserialize) + die("cannot mix --serialize and --deserialize"); + do_implicit_deserialize = 0; + + do_serialize = 1; + return 0; +} + +/* + * --deserialize | --deserialize= | + * --no-deserialize + * + * Request that we deserialize status data from some existing resource + * rather than performing a status scan. + * + * The input source can come from stdin or a path given here -- or be + * inherited from the config settings. + */ +static int opt_parse_deserialize(const struct option *opt UNUSED, const char *arg, int unset) +{ + if (unset) { + do_implicit_deserialize = 0; + do_explicit_deserialize = 0; + } else { + if (do_serialize) + die("cannot mix --serialize and --deserialize"); + if (arg) { + /* override config or stdin */ + free(deserialize_path); + deserialize_path = xstrdup(arg); + } + if (deserialize_path && *deserialize_path + && (access(deserialize_path, R_OK) != 0)) + die("cannot find serialization file '%s'", + deserialize_path); + + do_explicit_deserialize = 1; + } + + return 0; +} + static int opt_parse_m(const struct option *opt, const char *arg, int unset) { struct strbuf *buf = opt->value; @@ -1208,6 +1272,8 @@ static enum untracked_status_type parse_untracked_setting_name(const char *u) return SHOW_NORMAL_UNTRACKED_FILES; else if (!strcmp(u, "all")) return SHOW_ALL_UNTRACKED_FILES; + else if (!strcmp(u,"complete")) + return SHOW_COMPLETE_UNTRACKED_FILES; else return SHOW_UNTRACKED_FILES_ERROR; } @@ -1503,6 +1569,19 @@ static int git_status_config(const char *k, const char *v, s->relative_paths = git_config_bool(k, v); return 0; } + if (!strcmp(k, "status.deserializepath")) { + /* + * Automatically assume deserialization if this is + * set in the config and the file exists. Do not + * complain if the file does not exist, because we + * silently fall back to normal mode. + */ + if (v && *v && access(v, R_OK) == 0) { + do_implicit_deserialize = 1; + deserialize_path = xstrdup(v); + } + return 0; + } if (!strcmp(k, "status.showuntrackedfiles")) { enum untracked_status_type u; @@ -1542,7 +1621,8 @@ struct repository *repo UNUSED) static const char *rename_score_arg = (const char *)-1; static struct wt_status s; unsigned int progress_flag = 0; - int fd; + int try_deserialize; + int fd = -1; struct object_id oid; static struct option builtin_status_options[] = { OPT__VERBOSE(&verbose, N_("be verbose")), @@ -1557,6 +1637,12 @@ struct repository *repo UNUSED) OPT_CALLBACK_F(0, "porcelain", &status_format, N_("version"), N_("machine-readable output"), PARSE_OPT_OPTARG, opt_parse_porcelain), + OPT_CALLBACK_F(0, "serialize", &status_format, + N_("version"), N_("serialize raw status data to stdout"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, opt_parse_serialize), + OPT_CALLBACK_F(0, "deserialize", NULL, + N_("path"), N_("deserialize raw status data from file"), + PARSE_OPT_OPTARG, opt_parse_deserialize), OPT_SET_INT(0, "long", &status_format, N_("show status in long format (default)"), STATUS_FORMAT_LONG), @@ -1618,10 +1704,26 @@ struct repository *repo UNUSED) s.show_untracked_files == SHOW_NO_UNTRACKED_FILES) die(_("Unsupported combination of ignored and untracked-files arguments")); + if (s.show_untracked_files == SHOW_COMPLETE_UNTRACKED_FILES && + s.show_ignored_mode == SHOW_NO_IGNORED) + die(_("Complete Untracked only supported with ignored files")); + parse_pathspec(&s.pathspec, 0, PATHSPEC_PREFER_FULL, prefix, argv); + /* + * If we want to try to deserialize status data from a cache file, + * we need to re-order the initialization code. The problem is that + * this makes for a very nasty diff and causes merge conflicts as we + * carry it forward. And it easy to mess up the merge, so we + * duplicate some code here to hopefully reduce conflicts. + */ + try_deserialize = (!do_serialize && + (do_implicit_deserialize || do_explicit_deserialize)); + if (try_deserialize) + goto skip_init; + enable_fscache(0); if (status_format != STATUS_FORMAT_PORCELAIN && status_format != STATUS_FORMAT_PORCELAIN_V2) @@ -1636,6 +1738,7 @@ struct repository *repo UNUSED) else fd = -1; +skip_init: s.is_initial = repo_get_oid(the_repository, s.reference, &oid) ? 1 : 0; if (!s.is_initial) oidcpy(&s.oid_commit, &oid); @@ -1652,6 +1755,24 @@ struct repository *repo UNUSED) s.rename_score = parse_rename_score(&rename_score_arg); } + if (try_deserialize) { + if (s.relative_paths) + s.prefix = prefix; + + if (wt_status_deserialize(&s, deserialize_path) == DESERIALIZE_OK) + return 0; + + /* deserialize failed, so force the initialization we skipped above. */ + enable_fscache(1); + repo_read_index_preload(the_repository, &s.pathspec, 0); + refresh_index(the_repository->index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL); + + if (use_optional_locks()) + fd = repo_hold_locked_index(the_repository, &index_lock, 0); + else + fd = -1; + } + wt_status_collect(&s); if (0 <= fd) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 73abea31b428f3..e3152957d4d858 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1837,7 +1837,7 @@ _git_clone () esac } -__git_untracked_file_modes="all no normal" +__git_untracked_file_modes="all no normal complete" __git_trailer_tokens () { diff --git a/meson.build b/meson.build index 6876d78dfd77d5..62363511c4eaad 100644 --- a/meson.build +++ b/meson.build @@ -540,6 +540,8 @@ libgit_sources = [ 'write-or-die.c', 'ws.c', 'wt-status.c', + 'wt-status-deserialize.c', + 'wt-status-serialize.c', 'xdiff-interface.c', 'xdiff/xdiffi.c', 'xdiff/xemit.c', diff --git a/pkt-line.c b/pkt-line.c index fc583feb26510d..a2d9c9c656fb50 100644 --- a/pkt-line.c +++ b/pkt-line.c @@ -230,7 +230,7 @@ static int do_packet_write(const int fd_out, const char *buf, size_t size, return 0; } -static int packet_write_gently(const int fd_out, const char *buf, size_t size) +int packet_write_gently(const int fd_out, const char *buf, size_t size) { struct strbuf err = STRBUF_INIT; if (do_packet_write(fd_out, buf, size, &err)) { diff --git a/pkt-line.h b/pkt-line.h index 3b33cc64f34dcc..10fd9a812e1935 100644 --- a/pkt-line.h +++ b/pkt-line.h @@ -29,6 +29,7 @@ void packet_write(int fd_out, const char *buf, size_t size); void packet_buf_write(struct strbuf *buf, const char *fmt, ...) __attribute__((format (printf, 2, 3))); int packet_flush_gently(int fd); int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); +int packet_write_gently(const int fd_out, const char *buf, size_t size); int write_packetized_from_fd_no_flush(int fd_in, int fd_out); int write_packetized_from_buf_no_flush_count(const char *src_in, size_t len, int fd_out, int *packet_counter); diff --git a/t/meson.build b/t/meson.build index cfb187c858458a..12892b2a4373d7 100644 --- a/t/meson.build +++ b/t/meson.build @@ -918,6 +918,8 @@ integration_tests = [ 't7519-status-fsmonitor.sh', 't7520-ignored-hook-warning.sh', 't7521-ignored-mode.sh', + 't7522-serialized-status.sh', + 't7523-status-complete-untracked.sh', 't7524-commit-summary.sh', 't7525-status-rename.sh', 't7526-commit-pathspec-file.sh', diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh new file mode 100755 index 00000000000000..283a98bdf750e6 --- /dev/null +++ b/t/t7522-serialized-status.sh @@ -0,0 +1,141 @@ +#!/bin/sh + +test_description='git serialized status tests' + +. ./test-lib.sh + +# This file includes tests for serializing / deserializing +# status data. These tests cover two basic features: +# +# [1] Because users can request different types of untracked-file +# and ignored file reporting, the cache data generated by +# serialize must use either the same untracked and ignored +# parameters as the later deserialize invocation; otherwise, +# the deserialize invocation must disregard the cached data +# and run a full scan itself. +# +# To increase the number of cases where the cached status can +# be used, we have added a "--untracked-file=complete" option +# that reports a superset or union of the results from the +# "-u normal" and "-u all". We combine this with a filter in +# deserialize to filter the results. +# +# Ignored file reporting is simpler in that is an all or +# nothing; there are no subsets. +# +# The tests here (in addition to confirming that a cache +# file can be generated and used by a subsequent status +# command) need to test this untracked-file filtering. +# +# [2] ensuring the status calls are using data from the status +# cache as expected. This includes verifying cached data +# is used when appropriate as well as falling back to +# performing a new status scan when the data in the cache +# is insufficient/known stale. + +test_expect_success 'setup' ' + git branch -M main && + cat >.gitignore <<-\EOF && + *.ign + ignored_dir/ + EOF + + mkdir tracked ignored_dir && + touch tracked_1.txt tracked/tracked_1.txt && + git add . && + test_tick && + git commit -m"Adding original file." && + mkdir untracked && + touch ignored.ign ignored_dir/ignored_2.txt \ + untracked_1.txt untracked/untracked_2.txt untracked/untracked_3.txt +' + +test_expect_success 'verify untracked-files=complete with no conversion' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-\EOF && + ? expect + ? serialized_status.dat + ? untracked/ + ? untracked/untracked_2.txt + ? untracked/untracked_3.txt + ? untracked_1.txt + ! ignored.ign + ! ignored_dir/ + EOF + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git status --porcelain=v2 --untracked-files=complete --ignored=matching --deserialize=serialized_status.dat >output && + test_cmp expect output +' + +test_expect_success 'verify untracked-files=complete to untracked-files=normal conversion' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-\EOF && + ? expect + ? serialized_status.dat + ? untracked/ + ? untracked_1.txt + EOF + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git status --porcelain=v2 --deserialize=serialized_status.dat >output && + test_cmp expect output +' + +test_expect_success 'verify untracked-files=complete to untracked-files=all conversion' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-\EOF && + ? expect + ? serialized_status.dat + ? untracked/untracked_2.txt + ? untracked/untracked_3.txt + ? untracked_1.txt + ! ignored.ign + ! ignored_dir/ + EOF + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git status --porcelain=v2 --untracked-files=all --ignored=matching --deserialize=serialized_status.dat >output && + test_cmp expect output +' + +test_expect_success 'verify serialized status with non-convertible ignore mode does new scan' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-\EOF && + ? expect + ? new_change.txt + ? output + ? serialized_status.dat + ? untracked/ + ? untracked_1.txt + ! ignored.ign + ! ignored_dir/ + EOF + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git status --porcelain=v2 --ignored --deserialize=serialized_status.dat >output && + test_cmp expect output +' + +test_expect_success 'verify serialized status handles path scopes' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-\EOF && + ? untracked/ + EOF + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git status --porcelain=v2 --deserialize=serialized_status.dat untracked >output && + test_cmp expect output +' + +test_done diff --git a/t/t7523-status-complete-untracked.sh b/t/t7523-status-complete-untracked.sh new file mode 100755 index 00000000000000..f79611fc024f48 --- /dev/null +++ b/t/t7523-status-complete-untracked.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +test_description='git status untracked complete tests' + +. ./test-lib.sh + +test_expect_success 'setup' ' + cat >.gitignore <<-\EOF && + *.ign + ignored_dir/ + EOF + + mkdir tracked ignored_dir && + touch tracked_1.txt tracked/tracked_1.txt && + git add . && + test_tick && + git commit -m"Adding original file." && + mkdir untracked && + touch ignored.ign ignored_dir/ignored_2.txt \ + untracked_1.txt untracked/untracked_2.txt untracked/untracked_3.txt +' + +test_expect_success 'verify untracked-files=complete' ' + cat >expect <<-\EOF && + ? expect + ? output + ? untracked/ + ? untracked/untracked_2.txt + ? untracked/untracked_3.txt + ? untracked_1.txt + ! ignored.ign + ! ignored_dir/ + EOF + + git status --porcelain=v2 --untracked-files=complete --ignored >output && + test_cmp expect output +' + +test_done diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c new file mode 100644 index 00000000000000..90174bb3d1fc51 --- /dev/null +++ b/wt-status-deserialize.c @@ -0,0 +1,622 @@ +#define USE_THE_REPOSITORY_VARIABLE +#include "git-compat-util.h" +#include "environment.h" +#include "hex.h" +#include "hash.h" +#include "wt-status.h" +#include "pkt-line.h" +#include "trace.h" +#include "statinfo.h" +#include "hex.h" + +static struct trace_key trace_deserialize = TRACE_KEY_INIT(DESERIALIZE); + +enum deserialize_parse_strategy { + DESERIALIZE_STRATEGY_AS_IS, + DESERIALIZE_STRATEGY_SKIP, + DESERIALIZE_STRATEGY_NORMAL, + DESERIALIZE_STRATEGY_ALL +}; + +static int check_path_contains(const char *out, int out_len, const char *in, int in_len) +{ + return (out_len > 0 && + out_len < in_len && + (out[out_len - 1] == '/') && + !memcmp(out, in, out_len)); +} + +static const char *my_packet_read_line(int fd, int *line_len) +{ + static char buf[LARGE_PACKET_MAX]; + + *line_len = packet_read(fd, buf, sizeof(buf), + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_GENTLE_ON_EOF); + return (*line_len > 0) ? buf : NULL; +} + +/* + * mtime_reported contains the mtime of the index when the + * serialization snapshot was computed. + * + * mtime_observed_on_disk contains the mtime of the index now. + * + * If these 2 times are different, then the .git/index has + * changed since the serialization cache was created and we + * must reject the cache because anything could have changed. + * + * If they are the same, we continue trying to use the cache. + */ +static int my_validate_index(const char *path, const struct cache_time *mtime_reported) +{ + struct stat st; + struct cache_time mtime_observed_on_disk; + + if (lstat(path, &st)) { + trace_printf_key(&trace_deserialize, "could not stat index"); + return DESERIALIZE_ERR; + } + mtime_observed_on_disk.sec = st.st_mtime; + mtime_observed_on_disk.nsec = ST_MTIME_NSEC(st); + if ((mtime_observed_on_disk.sec != mtime_reported->sec) || + (mtime_observed_on_disk.nsec != mtime_reported->nsec)) { + trace_printf_key(&trace_deserialize, "index mtime changed [des %d.%d][obs %d.%d]", + mtime_reported->sec, mtime_reported->nsec, + mtime_observed_on_disk.sec, mtime_observed_on_disk.nsec); + return DESERIALIZE_ERR; + } + + return DESERIALIZE_OK; +} + +static int wt_deserialize_v1_header(struct wt_status *s, int fd) +{ + struct cache_time index_mtime; + int line_len, nr_fields; + const char *line; + const char *arg; + + /* + * parse header lines up to the first flush packet. + */ + while ((line = my_packet_read_line(fd, &line_len))) { + + if (skip_prefix(line, "index_mtime ", &arg)) { + nr_fields = sscanf(arg, "%u %u", + &index_mtime.sec, + &index_mtime.nsec); + if (nr_fields != 2) { + trace_printf_key(&trace_deserialize, "invalid index_mtime (%d) '%s'", + nr_fields, line); + return DESERIALIZE_ERR; + } + continue; + } + + if (skip_prefix(line, "is_initial ", &arg)) { + s->is_initial = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "branch ", &arg)) { + s->branch = xstrdup(arg); + continue; + } + if (skip_prefix(line, "reference ", &arg)) { + s->reference = xstrdup(arg); + continue; + } + /* pathspec */ + /* verbose */ + /* amend */ + if (skip_prefix(line, "whence ", &arg)) { + s->whence = (int)strtol(arg, NULL, 10); + continue; + } + /* nowarn */ + /* use_color */ + /* no_gettext */ + /* display_comment_prefix */ + /* relative_paths */ + /* submodule_summary */ + if (skip_prefix(line, "show_ignored_mode ", &arg)) { + s->show_ignored_mode = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "show_untracked_files ", &arg)) { + s->show_untracked_files = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "ignore_submodule_arg ", &arg)) { + s->ignore_submodule_arg = xstrdup(arg); + continue; + } + /* color_palette */ + /* colopts */ + /* null_termination */ + /* commit_template */ + /* show_branch */ + /* show_stash */ + if (skip_prefix(line, "hints ", &arg)) { + s->hints = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "detect_rename ", &arg)) { + s->detect_rename = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "rename_score ", &arg)) { + s->rename_score = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "rename_limit ", &arg)) { + s->rename_limit = (int)strtol(arg, NULL, 10); + continue; + } + /* status_format */ + if (skip_prefix(line, "sha1_commit ", &arg)) { + if (get_oid_hex(arg, &s->oid_commit)) { + trace_printf_key(&trace_deserialize, "invalid sha1_commit"); + return DESERIALIZE_ERR; + } + continue; + } + if (skip_prefix(line, "committable ", &arg)) { + s->committable = (int)strtol(arg, NULL, 10); + continue; + } + if (skip_prefix(line, "workdir_dirty ", &arg)) { + s->workdir_dirty = (int)strtol(arg, NULL, 10); + continue; + } + /* prefix */ + + trace_printf_key(&trace_deserialize, "unexpected line '%s'", line); + return DESERIALIZE_ERR; + } + + return my_validate_index(s->index_file, &index_mtime); +} + +/* + * Build a string-list of (count) lines from the input. + */ +static int wt_deserialize_v1_changed_items(struct wt_status *s, int fd, int count UNUSED) +{ + struct wt_status_serialize_data *sd; + char *p; + int line_len; + const char *line; + struct string_list_item *item; + + memset(&s->change, 0, sizeof(s->change)); + s->change.strdup_strings = 1; + + /* + * + + * + * + * NUL [] NUL + */ + while ((line = my_packet_read_line(fd, &line_len))) { + struct wt_status_change_data *d = xcalloc(1, sizeof(*d)); + sd = (struct wt_status_serialize_data *)line; + + d->worktree_status = ntohl(sd->fixed.worktree_status); + d->index_status = ntohl(sd->fixed.index_status); + d->stagemask = ntohl(sd->fixed.stagemask); + d->rename_status = ntohl(sd->fixed.rename_status); + d->rename_score = ntohl(sd->fixed.rename_score); + d->mode_head = ntohl(sd->fixed.mode_head); + d->mode_index = ntohl(sd->fixed.mode_index); + d->mode_worktree = ntohl(sd->fixed.mode_worktree); + d->dirty_submodule = ntohl(sd->fixed.dirty_submodule); + d->new_submodule_commits = ntohl(sd->fixed.new_submodule_commits); + oidcpy(&d->oid_head, &sd->fixed.oid_head); + oidcpy(&d->oid_index, &sd->fixed.oid_index); + + p = sd->variant; + item = string_list_append(&s->change, p); + p += strlen(p) + 1; + if (*p) + d->rename_source = xstrdup(p); + item->util = d; + + trace_printf_key( + &trace_deserialize, + "change: %d %d %d %d %d %o %o %o %d %d %s %s '%s' '%s'", + d->worktree_status, + d->index_status, + d->stagemask, + d->rename_status, + d->rename_score, + d->mode_head, + d->mode_index, + d->mode_worktree, + d->dirty_submodule, + d->new_submodule_commits, + oid_to_hex(&d->oid_head), + oid_to_hex(&d->oid_index), + item->string, + (d->rename_source ? d->rename_source : "")); + } + + return DESERIALIZE_OK; +} + +static int wt_deserialize_v1_untracked_items(struct wt_status *s, + int fd, + int count UNUSED, + enum deserialize_parse_strategy strategy) +{ + int line_len; + const char *line; + char *out = NULL; + int out_len = 0; + + memset(&s->untracked, 0, sizeof(s->untracked)); + s->untracked.strdup_strings = 1; + + /* + * + + * + */ + while ((line = my_packet_read_line(fd, &line_len))) { + if (strategy == DESERIALIZE_STRATEGY_AS_IS) + string_list_append(&s->untracked, line); + if (strategy == DESERIALIZE_STRATEGY_SKIP) + continue; + if (strategy == DESERIALIZE_STRATEGY_NORMAL) { + + /* Only add "normal" entries to list */ + if (out && + check_path_contains(out, out_len, line, line_len)) { + continue; + } + else { + out = string_list_append(&s->untracked, line)->string; + out_len = line_len; + } + } + if (strategy == DESERIALIZE_STRATEGY_ALL) { + /* Only add "all" entries to list */ + if (line[line_len - 1] != '/') + string_list_append(&s->untracked, line); + } + } + + return DESERIALIZE_OK; +} + +static int wt_deserialize_v1_ignored_items(struct wt_status *s, + int fd, + int count UNUSED, + enum deserialize_parse_strategy strategy) +{ + int line_len; + const char *line; + + memset(&s->ignored, 0, sizeof(s->ignored)); + s->ignored.strdup_strings = 1; + + /* + * + + * + */ + while ((line = my_packet_read_line(fd, &line_len))) { + if (strategy == DESERIALIZE_STRATEGY_AS_IS) + string_list_append(&s->ignored, line); + else + continue; + } + + return DESERIALIZE_OK; +} + +static int validate_untracked_files_arg(enum untracked_status_type cmd, + enum untracked_status_type des, + enum deserialize_parse_strategy *strategy) +{ + *strategy = DESERIALIZE_STRATEGY_AS_IS; + + if (cmd == des) { + *strategy = DESERIALIZE_STRATEGY_AS_IS; + } else if (cmd == SHOW_NO_UNTRACKED_FILES) { + *strategy = DESERIALIZE_STRATEGY_SKIP; + } else if (des == SHOW_COMPLETE_UNTRACKED_FILES) { + if (cmd == SHOW_ALL_UNTRACKED_FILES) + *strategy = DESERIALIZE_STRATEGY_ALL; + else if (cmd == SHOW_NORMAL_UNTRACKED_FILES) + *strategy = DESERIALIZE_STRATEGY_NORMAL; + } else { + return DESERIALIZE_ERR; + } + + return DESERIALIZE_OK; +} + +static int validate_ignored_files_arg(enum show_ignored_type cmd, + enum show_ignored_type des, + enum deserialize_parse_strategy *strategy) +{ + *strategy = DESERIALIZE_STRATEGY_AS_IS; + + if (cmd == SHOW_NO_IGNORED) { + *strategy = DESERIALIZE_STRATEGY_SKIP; + } + else if (cmd != des) { + return DESERIALIZE_ERR; + } + + return DESERIALIZE_OK; +} + +static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, int fd) +{ + int line_len; + const char *line; + const char *arg; + int nr_changed = 0; + int nr_untracked = 0; + int nr_ignored = 0; + + enum deserialize_parse_strategy ignored_strategy = DESERIALIZE_STRATEGY_AS_IS, untracked_strategy = DESERIALIZE_STRATEGY_AS_IS; + + if (wt_deserialize_v1_header(s, fd) == DESERIALIZE_ERR) + return DESERIALIZE_ERR; + + /* + * We now have the header parsed. Look at the command args (as passed in), and see how to parse + * the serialized data + */ + if (validate_untracked_files_arg(cmd_s->show_untracked_files, s->show_untracked_files, &untracked_strategy)) { + trace_printf_key(&trace_deserialize, "reject: show_untracked_file: command: %d, serialized : %d", + cmd_s->show_untracked_files, + s->show_untracked_files); + return DESERIALIZE_ERR; + } + + if (validate_ignored_files_arg(cmd_s->show_ignored_mode, s->show_ignored_mode, &ignored_strategy)) { + trace_printf_key(&trace_deserialize, "reject: show_ignored_mode: command: %d, serialized: %d", + cmd_s->show_ignored_mode, + s->show_ignored_mode); + return DESERIALIZE_ERR; + } + + /* + * [ [+] ] + * [ [+] ] + * [ [+] ] + */ + while ((line = my_packet_read_line(fd, &line_len))) { + if (skip_prefix(line, "changed ", &arg)) { + nr_changed = (int)strtol(arg, NULL, 10); + if (wt_deserialize_v1_changed_items(s, fd, nr_changed) + == DESERIALIZE_ERR) + return DESERIALIZE_ERR; + continue; + } + if (skip_prefix(line, "untracked ", &arg)) { + nr_untracked = (int)strtol(arg, NULL, 10); + if (wt_deserialize_v1_untracked_items(s, fd, nr_untracked, untracked_strategy) + == DESERIALIZE_ERR) + return DESERIALIZE_ERR; + continue; + } + if (skip_prefix(line, "ignored ", &arg)) { + nr_ignored = (int)strtol(arg, NULL, 10); + if (wt_deserialize_v1_ignored_items(s, fd, nr_ignored, ignored_strategy) + == DESERIALIZE_ERR) + return DESERIALIZE_ERR; + continue; + } + trace_printf_key(&trace_deserialize, "unexpected line '%s'", line); + return DESERIALIZE_ERR; + } + + return DESERIALIZE_OK; +} + +static int wt_deserialize_parse(const struct wt_status *cmd_s, struct wt_status *s, int fd) +{ + int line_len; + const char *line; + const char *arg; + + if ((line = my_packet_read_line(fd, &line_len)) && + (skip_prefix(line, "version ", &arg))) { + int version = (int)strtol(arg, NULL, 10); + if (version == 1) + return wt_deserialize_v1(cmd_s, s, fd); + } + trace_printf_key(&trace_deserialize, "missing/unsupported version"); + return DESERIALIZE_ERR; +} + +static inline int my_strcmp_null(const char *a, const char *b) +{ + const char *alt_a = (a) ? a : ""; + const char *alt_b = (b) ? b : ""; + + return strcmp(alt_a, alt_b); +} + +static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *des_s, int fd) +{ + /* + * Check the path spec on the current command + */ + if (cmd_s->pathspec.nr > 1) { + trace_printf_key(&trace_deserialize, "reject: multiple pathspecs"); + return DESERIALIZE_ERR; + } + + /* + * If we have a pathspec, but it maches the root (e.g. no filtering) + * then this is OK. + */ + if (cmd_s->pathspec.nr == 1 && + my_strcmp_null(cmd_s->pathspec.items[0].match, "")) { + trace_printf_key(&trace_deserialize, "reject: pathspec"); + return DESERIALIZE_ERR; + } + + /* + * Copy over some required fields from the current command. + */ + des_s->repo = cmd_s->repo; + des_s->index_file = cmd_s->index_file; + + /* + * Deserialize cached status + */ + if (wt_deserialize_parse(cmd_s, des_s, fd) == DESERIALIZE_ERR) + return DESERIALIZE_ERR; + + /* + * Compare fields in cmd_s with those observed in des_s and + * complain if they are incompatible (such as different "-u" + * or "--ignored" settings). + */ + if (cmd_s->is_initial != des_s->is_initial) { + trace_printf_key(&trace_deserialize, "reject: is_initial"); + return DESERIALIZE_ERR; + } + if (my_strcmp_null(cmd_s->branch, des_s->branch)) { + trace_printf_key(&trace_deserialize, "reject: branch"); + return DESERIALIZE_ERR; + } + if (my_strcmp_null(cmd_s->reference, des_s->reference)) { + trace_printf_key(&trace_deserialize, "reject: reference"); + return DESERIALIZE_ERR; + } + /* verbose */ + /* amend */ + if (cmd_s->whence != des_s->whence) { + trace_printf_key(&trace_deserialize, "reject: whence"); + return DESERIALIZE_ERR; + } + /* nowarn */ + /* use_color */ + /* no_gettext */ + /* display_comment_prefix */ + /* relative_paths */ + /* submodule_summary */ + + /* show_ignored_files - already validated */ + /* show_untrackes_files - already validated */ + + /* + * Submodules are not supported by status serialization. + * The status will not be serialized if it contains submodules, + * and so this check is not needed. + * + * if (my_strcmp_null(cmd_s->ignore_submodule_arg, des_s->ignore_submodule_arg)) { + * trace_printf_key(&trace_deserialize, "reject: ignore_submodule_arg"); + * return DESERIALIZE_ERR; + * } + */ + + /* color_palette */ + /* colopts */ + /* null_termination */ + /* commit_template */ + /* show_branch */ + /* show_stash */ + /* hints */ + if (cmd_s->detect_rename != des_s->detect_rename) { + trace_printf_key(&trace_deserialize, "reject: detect_rename"); + return DESERIALIZE_ERR; + } + if (cmd_s->rename_score != des_s->rename_score) { + trace_printf_key(&trace_deserialize, "reject: rename_score"); + return DESERIALIZE_ERR; + } + if (cmd_s->rename_limit != des_s->rename_limit) { + trace_printf_key(&trace_deserialize, "reject: rename_limit"); + return DESERIALIZE_ERR; + } + /* status_format */ + if (!oideq(&cmd_s->oid_commit, &des_s->oid_commit)) { + trace_printf_key(&trace_deserialize, "reject: sha1_commit"); + return DESERIALIZE_ERR; + } + + /* + * Copy over display-related fields from the current command. + */ + des_s->verbose = cmd_s->verbose; + /* amend */ + /* whence */ + des_s->nowarn = cmd_s->nowarn; + des_s->use_color = cmd_s->use_color; + des_s->no_gettext = cmd_s->no_gettext; + des_s->display_comment_prefix = cmd_s->display_comment_prefix; + des_s->relative_paths = cmd_s->relative_paths; + des_s->submodule_summary = cmd_s->submodule_summary; + memcpy(des_s->color_palette, cmd_s->color_palette, + sizeof(char)*WT_STATUS_MAXSLOT*COLOR_MAXLEN); + des_s->colopts = cmd_s->colopts; + des_s->null_termination = cmd_s->null_termination; + /* commit_template */ + des_s->show_branch = cmd_s->show_branch; + des_s->show_stash = cmd_s->show_stash; + /* hints */ + des_s->status_format = cmd_s->status_format; + des_s->fp = cmd_s->fp; + if (cmd_s->prefix && *cmd_s->prefix) + des_s->prefix = xstrdup(cmd_s->prefix); + + return DESERIALIZE_OK; +} + + +/* + * Read raw serialized status data from the given file + * + * Verify that the args specified in the current command + * are compatible with the deserialized data (such as "-uno"). + * + * Copy display-related fields from the current command + * into the deserialized data (so that the user can request + * long or short as they please). + */ +int wt_status_deserialize(const struct wt_status *cmd_s, + const char *path) +{ + struct wt_status des_s; + int result; + struct string_list_item *change; + + if (path && *path && strcmp(path, "0")) { + int fd = xopen(path, O_RDONLY); + if (fd == -1) { + trace_printf_key(&trace_deserialize, "could not read '%s'", path); + return DESERIALIZE_ERR; + } + trace_printf_key(&trace_deserialize, "reading serialization file '%s'", path); + result = wt_deserialize_fd(cmd_s, &des_s, fd); + close(fd); + } else { + trace_printf_key(&trace_deserialize, "reading stdin"); + result = wt_deserialize_fd(cmd_s, &des_s, 0); + } + + if (result == DESERIALIZE_OK) { + wt_status_get_state(cmd_s->repo, &des_s.state, des_s.branch && + !strcmp(des_s.branch, "HEAD")); + wt_status_print(&des_s); + } + + free(des_s.branch); + free((char *)des_s.reference); + for_each_string_list_item(change, &des_s.change) { + struct wt_status_change_data *d = change->util; + if (d) + free(d->rename_source); + } + string_list_clear(&des_s.change, 1); + string_list_clear(&des_s.ignored, 1); + string_list_clear(&des_s.untracked, 1); + return result; +} diff --git a/wt-status-serialize.c b/wt-status-serialize.c new file mode 100644 index 00000000000000..a0dcf8e65ed44e --- /dev/null +++ b/wt-status-serialize.c @@ -0,0 +1,219 @@ +#include "git-compat-util.h" +#include "hex.h" +#include "repository.h" +#include "wt-status.h" +#include "pkt-line.h" +#include "trace.h" +#include "read-cache-ll.h" + +static struct trace_key trace_serialize = TRACE_KEY_INIT(SERIALIZE); + +/* + * Write V1 header fields. + */ +static void wt_serialize_v1_header(struct wt_status *s, int fd) +{ + /* + * Write select fields from the current index to help + * the deserializer recognize a stale data set. + */ + packet_write_fmt(fd, "index_mtime %d %d\n", + s->repo->index->timestamp.sec, + s->repo->index->timestamp.nsec); + + /* + * Write data from wt_status to qualify this status report. + * That is, if this run specified "-uno", the consumer of + * our serialization should know that. + */ + packet_write_fmt(fd, "is_initial %d\n", s->is_initial); + if (s->branch) + packet_write_fmt(fd, "branch %s\n", s->branch); + if (s->reference) + packet_write_fmt(fd, "reference %s\n", s->reference); + /* pathspec */ + /* verbose */ + /* amend */ + packet_write_fmt(fd, "whence %d\n", s->whence); + /* nowarn */ + /* use_color */ + /* no_gettext */ + /* display_comment_prefix */ + /* relative_paths */ + /* submodule_summary */ + packet_write_fmt(fd, "show_ignored_mode %d\n", s->show_ignored_mode); + packet_write_fmt(fd, "show_untracked_files %d\n", s->show_untracked_files); + if (s->ignore_submodule_arg) + packet_write_fmt(fd, "ignore_submodule_arg %s\n", s->ignore_submodule_arg); + /* color_palette */ + /* colopts */ + /* null_termination */ + /* commit_template */ + /* show_branch */ + /* show_stash */ + packet_write_fmt(fd, "hints %d\n", s->hints); + packet_write_fmt(fd, "detect_rename %d\n", s->detect_rename); + packet_write_fmt(fd, "rename_score %d\n", s->rename_score); + packet_write_fmt(fd, "rename_limit %d\n", s->rename_limit); + /* status_format */ + packet_write_fmt(fd, "sha1_commit %s\n", oid_to_hex(&s->oid_commit)); + packet_write_fmt(fd, "committable %d\n", s->committable); + packet_write_fmt(fd, "workdir_dirty %d\n", s->workdir_dirty); + /* prefix */ + packet_flush(fd); +} + +/* + * Print changed/unmerged items. + * We write raw (not c-quoted) pathname(s). The rename_source is only + * set when status computed a rename/copy. + * + * We ALWAYS write a final LF to the packet-line (for debugging) + * even though Linux pathnames allow LFs. + */ +static inline void wt_serialize_v1_changed(struct wt_status *s UNUSED, int fd, + struct string_list_item *item) +{ + struct wt_status_change_data *d = item->util; + struct wt_status_serialize_data sd; + char *begin; + char *end; + char *p; + int len_path, len_rename_source; + + trace_printf_key(&trace_serialize, + "change: %d %d %d %d %d %o %o %o %d %d %s %s '%s' '%s'", + d->worktree_status, + d->index_status, + d->stagemask, + d->rename_status, + d->rename_score, + d->mode_head, + d->mode_index, + d->mode_worktree, + d->dirty_submodule, + d->new_submodule_commits, + oid_to_hex(&d->oid_head), + oid_to_hex(&d->oid_index), + item->string, + (d->rename_source ? d->rename_source : "")); + + sd.fixed.worktree_status = htonl(d->worktree_status); + sd.fixed.index_status = htonl(d->index_status); + sd.fixed.stagemask = htonl(d->stagemask); + sd.fixed.rename_status = htonl(d->rename_status); + sd.fixed.rename_score = htonl(d->rename_score); + sd.fixed.mode_head = htonl(d->mode_head); + sd.fixed.mode_index = htonl(d->mode_index); + sd.fixed.mode_worktree = htonl(d->mode_worktree); + sd.fixed.dirty_submodule = htonl(d->dirty_submodule); + sd.fixed.new_submodule_commits = htonl(d->new_submodule_commits); + oidcpy(&sd.fixed.oid_head, &d->oid_head); + oidcpy(&sd.fixed.oid_index, &d->oid_index); + + begin = (char *)&sd; + end = begin + sizeof(sd); + + p = sd.variant; + + /* + * Write NUL [] NUL LF at the end of the buffer. + */ + len_path = strlen(item->string); + len_rename_source = d->rename_source ? strlen(d->rename_source) : 0; + + /* + * This is a bit of a hack, but I don't want to split the + * status detail record across multiple pkt-lines. + */ + if (p + len_path + 1 + len_rename_source + 1 + 1 >= end) + BUG("path to long to serialize '%s'", item->string); + + memcpy(p, item->string, len_path); + p += len_path; + *p++ = '\0'; + + if (len_rename_source) { + memcpy(p, d->rename_source, len_rename_source); + p += len_rename_source; + } + *p++ = '\0'; + *p++ = '\n'; + + if (packet_write_gently(fd, begin, (p - begin))) + BUG("cannot serialize '%s'", item->string); +} + +/* + * Write raw (not c-quoted) pathname for an untracked item. + * We ALWAYS write a final LF to the packet-line (for debugging) + * even though Linux pathnames allows LFs. That is, deserialization + * should use the packet-line length and omit the final LF. + */ +static inline void wt_serialize_v1_untracked(struct wt_status *s UNUSED, int fd, + struct string_list_item *item) +{ + packet_write_fmt(fd, "%s\n", item->string); +} + +/* + * Write raw (not c-quoted) pathname for an ignored item. + * We ALWAYS write a final LF to the packet-line (for debugging) + * even though Linux pathnames allows LFs. + */ +static inline void wt_serialize_v1_ignored(struct wt_status *s UNUSED, int fd, + struct string_list_item *item) +{ + packet_write_fmt(fd, "%s\n", item->string); +} + +/* + * Serialize the list of changes to stdout. The goal of this + * is to just serialize the key fields in wt_status so that a + * later command can rebuilt it and do the printing. + * + * We DO NOT include the contents of wt_status_state NOR + * current branch info. This info easily gets stale and + * is relatively quick for the status consumer to compute + * as necessary. + */ +void wt_status_serialize_v1(struct wt_status *s) +{ + int fd = 1; /* we always write to stdout */ + struct string_list_item *iter; + size_t k; + + /* + * version header must be first line. + */ + packet_write_fmt(fd, "version 1\n"); + + wt_serialize_v1_header(s, fd); + + if (s->change.nr > 0) { + packet_write_fmt(fd, "changed %"PRIuMAX"\n", (uintmax_t)s->change.nr); + for (k = 0; k < s->change.nr; k++) { + iter = &(s->change.items[k]); + wt_serialize_v1_changed(s, fd, iter); + } + packet_flush(fd); + } + + if (s->untracked.nr > 0) { + packet_write_fmt(fd, "untracked %"PRIuMAX"\n", (uintmax_t)s->untracked.nr); + for (k = 0; k < s->untracked.nr; k++) { + iter = &(s->untracked.items[k]); + wt_serialize_v1_untracked(s, fd, iter); + } + packet_flush(fd); + } + + if (s->ignored.nr > 0) { + packet_write_fmt(fd, "ignored %"PRIuMAX"\n", (uintmax_t)s->ignored.nr); + for (k = 0; k < s->ignored.nr; k++) { + iter = &(s->ignored.items[k]); + wt_serialize_v1_ignored(s, fd, iter); + } + packet_flush(fd); + } +} diff --git a/wt-status.c b/wt-status.c index 9302d8253f3989..13f89d192093b4 100644 --- a/wt-status.c +++ b/wt-status.c @@ -792,6 +792,9 @@ static void wt_status_collect_untracked(struct wt_status *s) if (s->show_untracked_files != SHOW_ALL_UNTRACKED_FILES) dir.flags |= DIR_SHOW_OTHER_DIRECTORIES | DIR_HIDE_EMPTY_DIRECTORIES; + if (s->show_untracked_files == SHOW_COMPLETE_UNTRACKED_FILES) + dir.flags |= DIR_KEEP_UNTRACKED_CONTENTS; + if (s->show_ignored_mode) { dir.flags |= DIR_SHOW_IGNORED_TOO; @@ -2620,6 +2623,9 @@ void wt_status_print(struct wt_status *s) case STATUS_FORMAT_LONG: wt_longstatus_print(s); break; + case STATUS_FORMAT_SERIALIZE_V1: + wt_status_serialize_v1(s); + break; } trace2_region_leave("status", "print", s->repo); diff --git a/wt-status.h b/wt-status.h index e40a27214a700d..8b922c4d67b1c2 100644 --- a/wt-status.h +++ b/wt-status.h @@ -4,6 +4,7 @@ #include "string-list.h" #include "color.h" #include "pathspec.h" +#include "pkt-line.h" #include "remote.h" struct repository; @@ -26,7 +27,8 @@ enum untracked_status_type { SHOW_UNTRACKED_FILES_ERROR = -1, SHOW_NO_UNTRACKED_FILES = 0, SHOW_NORMAL_UNTRACKED_FILES, - SHOW_ALL_UNTRACKED_FILES + SHOW_ALL_UNTRACKED_FILES, + SHOW_COMPLETE_UNTRACKED_FILES, }; enum show_ignored_type { @@ -74,6 +76,7 @@ enum wt_status_format { STATUS_FORMAT_SHORT, STATUS_FORMAT_PORCELAIN, STATUS_FORMAT_PORCELAIN_V2, + STATUS_FORMAT_SERIALIZE_V1, STATUS_FORMAT_UNSPECIFIED }; @@ -185,4 +188,52 @@ int require_clean_work_tree(struct repository *repo, int ignore_submodules, int gently); +#define DESERIALIZE_OK 0 +#define DESERIALIZE_ERR 1 + +struct wt_status_serialize_data_fixed +{ + uint32_t worktree_status; + uint32_t index_status; + uint32_t stagemask; + uint32_t rename_status; + uint32_t rename_score; + uint32_t mode_head; + uint32_t mode_index; + uint32_t mode_worktree; + uint32_t dirty_submodule; + uint32_t new_submodule_commits; + struct object_id oid_head; + struct object_id oid_index; +}; + +/* + * Consume the maximum amount of data possible in a + * packet-line record. This is overkill because we + * have at most 2 relative pathnames, but means we + * don't need to allocate a variable length structure. + */ +struct wt_status_serialize_data +{ + struct wt_status_serialize_data_fixed fixed; + char variant[LARGE_PACKET_DATA_MAX + - sizeof(struct wt_status_serialize_data_fixed)]; +}; + +/* + * Serialize computed status scan results using "version 1" format + * to the given file. + */ +void wt_status_serialize_v1(struct wt_status *s); + +/* + * Deserialize existing status results from the given file and + * populate a (new) "struct wt_status". Use the contents of "cmd_s" + * (computed from the command line arguments) to verify that the + * cached data is compatible and overlay various display-related + * fields. + */ +int wt_status_deserialize(const struct wt_status *cmd_s, + const char *path); + #endif /* STATUS_H */ From 06cba352f20db51f9c878bb4bddd10410e8c79fb Mon Sep 17 00:00:00 2001 From: Jameson Miller Date: Wed, 10 Jan 2018 11:56:26 -0500 Subject: [PATCH 092/229] Teach ahead-behind and serialized status to play nicely together --- t/t7522-serialized-status.sh | 34 +++++++++++++++++++++++++++++++++- wt-status-deserialize.c | 3 ++- wt-status-serialize.c | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index 283a98bdf750e6..0f5a33e2a23442 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -47,7 +47,13 @@ test_expect_success 'setup' ' git commit -m"Adding original file." && mkdir untracked && touch ignored.ign ignored_dir/ignored_2.txt \ - untracked_1.txt untracked/untracked_2.txt untracked/untracked_3.txt + untracked_1.txt untracked/untracked_2.txt untracked/untracked_3.txt && + + test_oid_cache <<-EOF + branch_oid sha1:68d4a437ea4c2de65800f48c053d4d543b55c410 + + branch_oid sha256:6b95e4b1ea911dad213f2020840f5e92d3066cf9e38cf35f79412ec58d409ce4 + EOF ' test_expect_success 'verify untracked-files=complete with no conversion' ' @@ -138,4 +144,30 @@ test_expect_success 'verify serialized status handles path scopes' ' test_cmp expect output ' +test_expect_success 'verify no-ahead-behind and serialized status integration' ' + test_when_finished "rm serialized_status.dat new_change.txt output" && + cat >expect <<-EOF && + # branch.oid $(test_oid branch_oid) + # branch.head alt_branch + # branch.upstream main + # branch.ab +1 -0 + ? expect + ? serialized_status.dat + ? untracked/ + ? untracked_1.txt + EOF + + git checkout -b alt_branch main --track >/dev/null && + touch alt_branch_changes.txt && + git add alt_branch_changes.txt && + test_tick && + git commit -m"New commit on alt branch" && + + git status --untracked-files=complete --ignored=matching --serialize >serialized_status.dat && + touch new_change.txt && + + git -c status.aheadBehind=false status --porcelain=v2 --branch --ahead-behind --deserialize=serialized_status.dat >output && + test_cmp expect output +' + test_done diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index 90174bb3d1fc51..52e3958bcf2eba 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -524,6 +524,7 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de /* show_branch */ /* show_stash */ /* hints */ + /* ahead_behind_flags */ if (cmd_s->detect_rename != des_s->detect_rename) { trace_printf_key(&trace_deserialize, "reject: detect_rename"); return DESERIALIZE_ERR; @@ -562,6 +563,7 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de des_s->show_branch = cmd_s->show_branch; des_s->show_stash = cmd_s->show_stash; /* hints */ + des_s->ahead_behind_flags = cmd_s->ahead_behind_flags; des_s->status_format = cmd_s->status_format; des_s->fp = cmd_s->fp; if (cmd_s->prefix && *cmd_s->prefix) @@ -570,7 +572,6 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de return DESERIALIZE_OK; } - /* * Read raw serialized status data from the given file * diff --git a/wt-status-serialize.c b/wt-status-serialize.c index a0dcf8e65ed44e..b7102ce80baf50 100644 --- a/wt-status-serialize.c +++ b/wt-status-serialize.c @@ -52,6 +52,7 @@ static void wt_serialize_v1_header(struct wt_status *s, int fd) /* show_branch */ /* show_stash */ packet_write_fmt(fd, "hints %d\n", s->hints); + /* ahead_behind_flags */ packet_write_fmt(fd, "detect_rename %d\n", s->detect_rename); packet_write_fmt(fd, "rename_score %d\n", s->rename_score); packet_write_fmt(fd, "rename_limit %d\n", s->rename_limit); From 83841fa40afbd922d5680750c42ceb6a77f03e7a Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 2 Feb 2018 14:17:05 -0500 Subject: [PATCH 093/229] status: serialize to path Teach status serialization to take an optional pathname on the command line to direct that cache data be written there rather than to stdout. When used this way, normal status results will still be written to stdout. When no path is given, only binary serialization data is written to stdout. Usage: git status --serialize[=] Signed-off-by: Jeff Hostetler --- Documentation/git-status.adoc | 10 ++++++---- builtin/commit.c | 36 ++++++++++++++++++++++++++--------- t/t7522-serialized-status.sh | 35 ++++++++++++++++++++++++++++++++++ wt-status-serialize.c | 5 ++--- wt-status.c | 2 +- wt-status.h | 2 +- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/Documentation/git-status.adoc b/Documentation/git-status.adoc index fedf86d32718eb..62254617dd7fc1 100644 --- a/Documentation/git-status.adoc +++ b/Documentation/git-status.adoc @@ -151,10 +151,12 @@ ignored, then the directory is not shown, but all contents are shown. threshold. See also linkgit:git-diff[1] `--find-renames`. ---serialize[=]:: - (EXPERIMENTAL) Serialize raw status results to stdout in a - format suitable for use by `--deserialize`. Valid values for - `` are "1" and "v1". +--serialize[=]:: + (EXPERIMENTAL) Serialize raw status results to a file or stdout + in a format suitable for use by `--deserialize`. If a path is + given, serialize data will be written to that path *and* normal + status output will be written to stdout. If path is omitted, + only binary serialization data will be written to stdout. --deserialize[=]:: (EXPERIMENTAL) Deserialize raw status results from a file or diff --git a/builtin/commit.c b/builtin/commit.c index 24bc6a5ffa2099..56486883180bb4 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -167,26 +167,34 @@ static int opt_parse_porcelain(const struct option *opt, const char *arg, int un } static int do_serialize = 0; +static char *serialize_path = NULL; + static int do_implicit_deserialize = 0; static int do_explicit_deserialize = 0; static char *deserialize_path = NULL; /* - * --serialize | --serialize=1 | --serialize=v1 + * --serialize | --serialize= + * + * Request that we serialize status output rather than or in addition to + * printing in any of the established formats. + * + * Without a path, we write binary serialization data to stdout (and omit + * the normal status output). * - * Request that we serialize our output rather than printing in - * any of the established formats. Optionally specify serialization - * version. + * With a path, we write binary serialization data to the and then + * write normal status output. */ static int opt_parse_serialize(const struct option *opt, const char *arg, int unset) { enum wt_status_format *value = (enum wt_status_format *)opt->value; if (unset || !arg) *value = STATUS_FORMAT_SERIALIZE_V1; - else if (!strcmp(arg, "v1") || !strcmp(arg, "1")) - *value = STATUS_FORMAT_SERIALIZE_V1; - else - die("unsupported serialize version '%s'", arg); + + if (arg) { + free(serialize_path); + serialize_path = xstrdup(arg); + } if (do_explicit_deserialize) die("cannot mix --serialize and --deserialize"); @@ -1638,7 +1646,7 @@ struct repository *repo UNUSED) N_("version"), N_("machine-readable output"), PARSE_OPT_OPTARG, opt_parse_porcelain), OPT_CALLBACK_F(0, "serialize", &status_format, - N_("version"), N_("serialize raw status data to stdout"), + N_("path"), N_("serialize raw status data to path or stdout"), PARSE_OPT_OPTARG | PARSE_OPT_NONEG, opt_parse_serialize), OPT_CALLBACK_F(0, "deserialize", NULL, N_("path"), N_("deserialize raw status data from file"), @@ -1781,6 +1789,16 @@ struct repository *repo UNUSED) if (s.relative_paths) s.prefix = prefix; + if (serialize_path) { + int fd_serialize = xopen(serialize_path, + O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd_serialize < 0) + die_errno(_("could not serialize to '%s'"), + serialize_path); + wt_status_serialize_v1(fd_serialize, &s); + close(fd_serialize); + } + wt_status_print(&s); wt_status_collect_free_buffers(&s); diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index 0f5a33e2a23442..2a81b4e625ee59 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -170,4 +170,39 @@ test_expect_success 'verify no-ahead-behind and serialized status integration' ' test_cmp expect output ' +test_expect_success 'verify new --serialize=path mode' ' + #test_when_finished "rm serialized_status.dat expect new_change.txt output.1 output.2" && + cat >expect <<-\EOF && + ? expect + ? output.1 + ? untracked/ + ? untracked_1.txt + EOF + + git checkout -b serialize_path_branch main --track >/dev/null && + touch alt_branch_changes.txt && + git add alt_branch_changes.txt && + test_tick && + git commit -m"New commit on serialize_path_branch" && + + git status --porcelain=v2 --serialize=serialized_status.dat >output.1 && + touch new_change.txt && + + git status --porcelain=v2 --deserialize=serialized_status.dat >output.2 && + test_cmp expect output.1 && + test_cmp expect output.2 +' + +test_expect_success 'renames' ' + git init -b main rename_test && + echo OLDNAME >rename_test/OLDNAME && + git -C rename_test add OLDNAME && + git -C rename_test commit -m OLDNAME && + git -C rename_test mv OLDNAME NEWNAME && + git -C rename_test status --serialize=renamed.dat >output.1 && + echo DIRT >rename_test/DIRT && + git -C rename_test status --deserialize=renamed.dat >output.2 && + test_cmp output.1 output.2 +' + test_done diff --git a/wt-status-serialize.c b/wt-status-serialize.c index b7102ce80baf50..179e76f8081fdf 100644 --- a/wt-status-serialize.c +++ b/wt-status-serialize.c @@ -169,7 +169,7 @@ static inline void wt_serialize_v1_ignored(struct wt_status *s UNUSED, int fd, } /* - * Serialize the list of changes to stdout. The goal of this + * Serialize the list of changes to the given file. The goal of this * is to just serialize the key fields in wt_status so that a * later command can rebuilt it and do the printing. * @@ -178,9 +178,8 @@ static inline void wt_serialize_v1_ignored(struct wt_status *s UNUSED, int fd, * is relatively quick for the status consumer to compute * as necessary. */ -void wt_status_serialize_v1(struct wt_status *s) +void wt_status_serialize_v1(int fd, struct wt_status *s) { - int fd = 1; /* we always write to stdout */ struct string_list_item *iter; size_t k; diff --git a/wt-status.c b/wt-status.c index 13f89d192093b4..2dcca6bc0bdea2 100644 --- a/wt-status.c +++ b/wt-status.c @@ -2624,7 +2624,7 @@ void wt_status_print(struct wt_status *s) wt_longstatus_print(s); break; case STATUS_FORMAT_SERIALIZE_V1: - wt_status_serialize_v1(s); + wt_status_serialize_v1(1, s); break; } diff --git a/wt-status.h b/wt-status.h index 8b922c4d67b1c2..2a56229b24127f 100644 --- a/wt-status.h +++ b/wt-status.h @@ -224,7 +224,7 @@ struct wt_status_serialize_data * Serialize computed status scan results using "version 1" format * to the given file. */ -void wt_status_serialize_v1(struct wt_status *s); +void wt_status_serialize_v1(int fd, struct wt_status *s); /* * Deserialize existing status results from the given file and From ea4b7cc500e8f4e3314dcbaf69157c84863a2da3 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 7 Feb 2018 10:59:03 -0500 Subject: [PATCH 094/229] status: reject deserialize in V2 and conflicts Teach status deserialize code to reject status cache when printing in porcelain V2 and there are unresolved conflicts in the cache file. A follow-on task might extend the cache format to include this additiona data. See code for longer explanation. Signed-off-by: Jeff Hostetler --- t/t7522-serialized-status.sh | 90 +++++++++++++++++++++++++++++++++++- wt-status-deserialize.c | 28 ++++++++++- 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index 2a81b4e625ee59..361afca94835e0 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -51,8 +51,14 @@ test_expect_success 'setup' ' test_oid_cache <<-EOF branch_oid sha1:68d4a437ea4c2de65800f48c053d4d543b55c410 + x_base sha1:587be6b4c3f93f93c489c0111bba5596147a26cb + x_ours sha1:b68025345d5301abad4d9ec9166f455243a0d746 + x_theirs sha1:975fbec8256d3e8a3797e7a3611380f27c49f4ac branch_oid sha256:6b95e4b1ea911dad213f2020840f5e92d3066cf9e38cf35f79412ec58d409ce4 + x_base sha256:14f5162e2fe3d240d0d37aaab0f90e4af9a7cfa79639f3bab005b5bfb4174d9f + x_ours sha256:3a404ba030a4afa912155c476a48a253d4b3a43d0098431b6d6ca6e554bd78fb + x_theirs sha256:44dc634218adec09e34f37839b3840bad8c6103693e9216626b32d00e093fa35 EOF ' @@ -171,7 +177,7 @@ test_expect_success 'verify no-ahead-behind and serialized status integration' ' ' test_expect_success 'verify new --serialize=path mode' ' - #test_when_finished "rm serialized_status.dat expect new_change.txt output.1 output.2" && + test_when_finished "rm serialized_status.dat expect new_change.txt output.1 output.2" && cat >expect <<-\EOF && ? expect ? output.1 @@ -193,6 +199,88 @@ test_expect_success 'verify new --serialize=path mode' ' test_cmp expect output.2 ' +test_expect_success 'merge conflicts' ' + + # create a merge conflict. + + git init -b main conflicts && + echo x >conflicts/x.txt && + git -C conflicts add x.txt && + git -C conflicts commit -m x && + git -C conflicts branch a && + git -C conflicts branch b && + git -C conflicts checkout a && + echo y >conflicts/x.txt && + git -C conflicts add x.txt && + git -C conflicts commit -m a && + git -C conflicts checkout b && + echo z >conflicts/x.txt && + git -C conflicts add x.txt && + git -C conflicts commit -m b && + test_must_fail git -C conflicts merge --no-commit a && + + # verify that regular status correctly identifies it + # in each format. + + cat >expect.v2 <observed.v2 && + test_cmp expect.v2 observed.v2 && + + cat >expect.long <..." to mark resolution) + both modified: x.txt + +no changes added to commit (use "git add" and/or "git commit -a") +EOF + git -C conflicts status --long >observed.long && + test_cmp expect.long observed.long && + + cat >expect.short <observed.short && + test_cmp expect.short observed.short && + + # save status data in serialized cache. + + git -C conflicts status --serialize >serialized && + + # make some dirt in the worktree so we can tell whether subsequent + # status commands used the cached data or did a fresh status. + + echo dirt >conflicts/dirt.txt && + + # run status using the cached data. + + git -C conflicts status --long --deserialize=../serialized >observed.long && + test_cmp expect.long observed.long && + + git -C conflicts status --short --deserialize=../serialized >observed.short && + test_cmp expect.short observed.short && + + # currently, the cached data does not have enough information about + # merge conflicts for porcelain V2 format. (And V2 format looks at + # the index to get that data, but the whole point of the serialization + # is to avoid reading the index unnecessarily.) So V2 always rejects + # the cached data when there is an unresolved conflict. + + cat >expect.v2.dirty <observed.v2 && + test_cmp expect.v2.dirty observed.v2 + +' + test_expect_success 'renames' ' git init -b main rename_test && echo OLDNAME >rename_test/OLDNAME && diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index 52e3958bcf2eba..293b9a71ecb99d 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -181,7 +181,8 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) /* * Build a string-list of (count) lines from the input. */ -static int wt_deserialize_v1_changed_items(struct wt_status *s, int fd, int count UNUSED) +static int wt_deserialize_v1_changed_items(const struct wt_status *cmd_s, + struct wt_status *s, int fd, int count UNUSED) { struct wt_status_serialize_data *sd; char *p; @@ -239,6 +240,29 @@ static int wt_deserialize_v1_changed_items(struct wt_status *s, int fd, int coun oid_to_hex(&d->oid_index), item->string, (d->rename_source ? d->rename_source : "")); + + if (d->stagemask && + cmd_s->status_format == STATUS_FORMAT_PORCELAIN_V2) { + /* + * We have an unresolved conflict and the user wants + * to see porcelain V2 output. The cached status data + * does not contain enough information for V2 (because + * the main status computation does not capture it). + * We only get a single change record for the file with + * a single SHA -- we don't get the stage [123] mode + * and SHA data. The V2 detail-line print code looks + * up this information directly from the index. The + * whole point of this serialization cache is to avoid + * reading the index, so the V2 print code gets zeros. + * So we reject the status cache and let the fallback + * code run. + */ + trace_printf_key( + &trace_deserialize, + "reject: V2 format and umerged file: %s", + item->string); + return DESERIALIZE_ERR; + } } return DESERIALIZE_OK; @@ -391,7 +415,7 @@ static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, while ((line = my_packet_read_line(fd, &line_len))) { if (skip_prefix(line, "changed ", &arg)) { nr_changed = (int)strtol(arg, NULL, 10); - if (wt_deserialize_v1_changed_items(s, fd, nr_changed) + if (wt_deserialize_v1_changed_items(cmd_s, s, fd, nr_changed) == DESERIALIZE_ERR) return DESERIALIZE_ERR; continue; From 9c63e85e2651dec7fcad63b23a5bd7bbfbf71309 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 20 Jul 2018 12:08:50 -0400 Subject: [PATCH 095/229] serialize-status: serialize global and repo-local exclude file metadata Changes to the global or repo-local excludes files can change the results returned by "git status" for untracked files. Therefore, it is important that the exclude-file values used during serialization are still current at the time of deserialization. Teach "git status --serialize" to report metadata on the user's global exclude file (which defaults to "$XDG_HOME/git/ignore") and for the repo-local excludes file (which is in ".git/info/excludes"). Serialize will record the pathnames and mtimes for these files in the serialization header (next to the mtime data for the .git/index file). Teach "git status --deserialize" to validate this new metadata. If either exclude file has changed since the serialization-cache-file was written, then deserialize will reject the cache file and force a full/normal status run. Signed-off-by: Jeff Hostetler --- wt-status-deserialize.c | 85 ++++++++++++++++++++++++++++ wt-status-serialize.c | 122 ++++++++++++++++++++++++++++++++++++++++ wt-status.h | 8 +++ 3 files changed, 215 insertions(+) diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index 293b9a71ecb99d..93961a6bf9f20d 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -8,6 +8,7 @@ #include "trace.h" #include "statinfo.h" #include "hex.h" +#include "path.h" static struct trace_key trace_deserialize = TRACE_KEY_INIT(DESERIALIZE); @@ -70,12 +71,69 @@ static int my_validate_index(const char *path, const struct cache_time *mtime_re return DESERIALIZE_OK; } +/* + * Use the given key and exclude pathname to compute a serialization header + * reflecting the current contents on disk. See if that matches the value + * computed for this key when the cache was written. Reject the cache if + * anything has changed. + */ +static int my_validate_excludes(const char *path, const char *key, const char *line) +{ + struct strbuf sb = STRBUF_INIT; + int r; + + wt_serialize_compute_exclude_header(&sb, key, path); + + r = (strcmp(line, sb.buf) ? DESERIALIZE_ERR : DESERIALIZE_OK); + + if (r == DESERIALIZE_ERR) + trace_printf_key(&trace_deserialize, + "%s changed [cached '%s'][observed '%s']", + key, line, sb.buf); + + strbuf_release(&sb); + return r; +} + +static int my_parse_core_excludes(const char *line) +{ + /* + * In dir.c:setup_standard_excludes() they use either the value of + * the "core.excludefile" variable (stored in the global "excludes_file" + * variable) -or- the default value "$XDG_HOME/git/ignore". This is done + * during wt_status_collect_untracked() which we are hoping to not call. + * + * Fake the setup here. + */ + + if (excludes_file) { + return my_validate_excludes(excludes_file, "core_excludes", line); + } else { + char *path = xdg_config_home("ignore"); + int r = my_validate_excludes(path, "core_excludes", line); + free(path); + return r; + } +} + +static int my_parse_repo_excludes(const char *line) +{ + char *path = repo_git_path(the_repository, "info/exclude"); + int r = my_validate_excludes(path, "repo_excludes", line); + free(path); + + return r; +} + static int wt_deserialize_v1_header(struct wt_status *s, int fd) { struct cache_time index_mtime; int line_len, nr_fields; const char *line; const char *arg; + int have_required_index_mtime = 0; + int have_required_core_excludes = 0; + int have_required_repo_excludes = 0; /* * parse header lines up to the first flush packet. @@ -91,6 +149,20 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) nr_fields, line); return DESERIALIZE_ERR; } + have_required_index_mtime = 1; + continue; + } + + if (skip_prefix(line, "core_excludes ", &arg)) { + if (my_parse_core_excludes(line) != DESERIALIZE_OK) + return DESERIALIZE_ERR; + have_required_core_excludes = 1; + continue; + } + if (skip_prefix(line, "repo_excludes ", &arg)) { + if (my_parse_repo_excludes(line) != DESERIALIZE_OK) + return DESERIALIZE_ERR; + have_required_repo_excludes = 1; continue; } @@ -175,6 +247,19 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) return DESERIALIZE_ERR; } + if (!have_required_index_mtime) { + trace_printf_key(&trace_deserialize, "missing '%s'", "index_mtime"); + return DESERIALIZE_ERR; + } + if (!have_required_core_excludes) { + trace_printf_key(&trace_deserialize, "missing '%s'", "core_excludes"); + return DESERIALIZE_ERR; + } + if (!have_required_repo_excludes) { + trace_printf_key(&trace_deserialize, "missing '%s'", "repo_excludes"); + return DESERIALIZE_ERR; + } + return my_validate_index(s->index_file, &index_mtime); } diff --git a/wt-status-serialize.c b/wt-status-serialize.c index 179e76f8081fdf..6bbd4e575176f4 100644 --- a/wt-status-serialize.c +++ b/wt-status-serialize.c @@ -1,13 +1,133 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" +#include "environment.h" #include "hex.h" #include "repository.h" #include "wt-status.h" #include "pkt-line.h" #include "trace.h" #include "read-cache-ll.h" +#include "path.h" static struct trace_key trace_serialize = TRACE_KEY_INIT(SERIALIZE); +/* + * Compute header record for exclude file using format: + * SP SP LF + */ +void wt_serialize_compute_exclude_header(struct strbuf *sb, + const char *key, + const char *path) +{ + struct stat st; + struct stat_data sd; + + memset(&sd, 0, sizeof(sd)); + + strbuf_setlen(sb, 0); + + if (!path || !*path) { + strbuf_addf(sb, "%s U (unset)", key); + } else if (lstat(path, &st) == -1) { + if (is_missing_file_error(errno)) + strbuf_addf(sb, "%s E (not-found) %s", key, path); + else + strbuf_addf(sb, "%s E (other) %s", key, path); + } else { + fill_stat_data(&sd, &st); + strbuf_addf(sb, "%s F %d %d %s", + key, sd.sd_mtime.sec, sd.sd_mtime.nsec, path); + } +} + +static void append_exclude_info(int fd, const char *path, const char *key) +{ + struct strbuf sb = STRBUF_INIT; + + wt_serialize_compute_exclude_header(&sb, key, path); + + packet_write_fmt(fd, "%s\n", sb.buf); + + strbuf_release(&sb); +} + +static void append_core_excludes_file_info(int fd) +{ + /* + * Write pathname and mtime of the core/global excludes file to + * the status cache header. Since a change in the global excludes + * will/may change the results reported by status, the deserialize + * code should be able to reject the status cache if the excludes + * file changes since when the cache was written. + * + * The "core.excludefile" setting defaults to $XDG_HOME/git/ignore + * and uses a global variable which should have been set during + * wt_status_collect_untracked(). + * + * See dir.c:setup_standard_excludes() + */ + append_exclude_info(fd, excludes_file, "core_excludes"); +} + +static void append_repo_excludes_file_info(int fd) +{ + /* + * Likewise, there is a per-repo excludes file in .git/info/excludes + * that can change the results reported by status. And the deserialize + * code needs to be able to reject the status cache if this file + * changes. + * + * See dir.c:setup_standard_excludes() and git_path_info_excludes(). + * We replicate the pathname construction here because of the static + * variables/functions used in dir.c. + */ + char *path = repo_git_path(the_repository, "info/exclude"); + + append_exclude_info(fd, path, "repo_excludes"); + + free(path); +} + +/* + * WARNING: The status cache attempts to preserve the essential in-memory + * status data after a status scan into a "serialization" (aka "status cache") + * file. It allows later "git status --deserialize=" instances to + * just print the cached status results without scanning the workdir (and + * without reading the index). + * + * The status cache file is valid as long as: + * [1] the set of functional command line options are the same (think "-u"). + * [2] repo-local and user-global configuration settings are compatible. + * [3] nothing in the workdir has changed. + * + * We rely on: + * [1.a] We remember the relevant (functional, non-display) command line + * arguments in the status cache header. + * [2.a] We use the mtime of the .git/index to detect staging changes. + * [2.b] We use the mtimes of the excludes files to detect changes that + * might affect untracked file reporting. + * + * But we need external help to verify [3]. + * [] This includes changes to tracked files. + * [] This includes changes to tracked .gitignore files that might change + * untracked file reporting. + * [] This includes the creation of new, untracked per-directory .gitignore + * files that might change untracked file reporting. + * + * [3.a] On GVFS repos, we rely on the GVFS service (mount) daemon to + * watch the filesystem and invalidate (delete) the status cache + * when anything changes inside the workdir. + * + * [3.b] TODO This problem is not solved for non-GVFS repos. + * [] It is possible that the untracked-cache index extension + * could help with this but that requires status to read the + * index to load the extension. + * [] It is possible that the new fsmonitor facility could also + * provide this information, but that to requires reading the + * index. + */ + /* * Write V1 header fields. */ @@ -20,6 +140,8 @@ static void wt_serialize_v1_header(struct wt_status *s, int fd) packet_write_fmt(fd, "index_mtime %d %d\n", s->repo->index->timestamp.sec, s->repo->index->timestamp.nsec); + append_core_excludes_file_info(fd); + append_repo_excludes_file_info(fd); /* * Write data from wt_status to qualify this status report. diff --git a/wt-status.h b/wt-status.h index 2a56229b24127f..83a6a27de5146f 100644 --- a/wt-status.h +++ b/wt-status.h @@ -236,4 +236,12 @@ void wt_status_serialize_v1(int fd, struct wt_status *s); int wt_status_deserialize(const struct wt_status *cmd_s, const char *path); +/* + * A helper routine for serialize and deserialize to compute + * metadata for the user-global and repo-local excludes files. + */ +void wt_serialize_compute_exclude_header(struct strbuf *sb, + const char *key, + const char *path); + #endif /* STATUS_H */ From cd3b84174b0a5054c20b417a163d5cecb7c0ce61 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 25 Jul 2018 14:49:37 -0400 Subject: [PATCH 096/229] status: deserialization wait Teach `git status --deserialize` to either wait indefintely or immediately fail if the status serialization cache file is stale. Signed-off-by: Jeff Hostetler --- Documentation/config/status.adoc | 16 +++++ builtin/commit.c | 59 ++++++++++++++- t/t7522-serialized-status.sh | 52 ++++++++++++++ wt-status-deserialize.c | 119 ++++++++++++++++++++++++++++--- wt-status.h | 12 +++- 5 files changed, 245 insertions(+), 13 deletions(-) diff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc index 7302b066644e73..4d863fdaaec2eb 100644 --- a/Documentation/config/status.adoc +++ b/Documentation/config/status.adoc @@ -83,3 +83,19 @@ status.deserializePath:: generated by `--serialize`. This will be overridden by `--deserialize=` on the command line. If the cache file is invalid or stale, git will fall-back and compute status normally. + +status.deserializeWait:: + EXPERIMENTAL, Specifies what `git status --deserialize` should do + if the serialization cache file is stale and whether it should + fall-back and compute status normally. This will be overridden by + `--deserialize-wait=` on the command line. ++ +-- +* `fail` - cause git to exit with an error when the status cache file +is stale; this is intended for testing and debugging. +* `block` - cause git to spin and periodically retry the cache file +every 100 ms; this is intended to help coordinate with another git +instance concurrently computing the cache file. +* `no` - to immediately fall-back if cache file is stale. This is the default. +* `` - time (in tenths of a second) to spin and retry. +-- diff --git a/builtin/commit.c b/builtin/commit.c index 56486883180bb4..ee31665c93aa61 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -173,6 +173,9 @@ static int do_implicit_deserialize = 0; static int do_explicit_deserialize = 0; static char *deserialize_path = NULL; +static enum wt_status_deserialize_wait implicit_deserialize_wait = DESERIALIZE_WAIT__UNSET; +static enum wt_status_deserialize_wait explicit_deserialize_wait = DESERIALIZE_WAIT__UNSET; + /* * --serialize | --serialize= * @@ -238,6 +241,40 @@ static int opt_parse_deserialize(const struct option *opt UNUSED, const char *ar return 0; } +static enum wt_status_deserialize_wait parse_dw(const char *arg) +{ + int tenths; + + if (!strcmp(arg, "fail")) + return DESERIALIZE_WAIT__FAIL; + else if (!strcmp(arg, "block")) + return DESERIALIZE_WAIT__BLOCK; + else if (!strcmp(arg, "no")) + return DESERIALIZE_WAIT__NO; + + /* + * Otherwise, assume it is a timeout in tenths of a second. + * If it contains a bogus value, atol() will return zero + * which is OK. + */ + tenths = atol(arg); + if (tenths < 0) + tenths = DESERIALIZE_WAIT__NO; + return tenths; +} + +static int opt_parse_deserialize_wait(const struct option *opt UNUSED, + const char *arg, + int unset) +{ + if (unset) + explicit_deserialize_wait = DESERIALIZE_WAIT__UNSET; + else + explicit_deserialize_wait = parse_dw(arg); + + return 0; +} + static int opt_parse_m(const struct option *opt, const char *arg, int unset) { struct strbuf *buf = opt->value; @@ -1590,6 +1627,13 @@ static int git_status_config(const char *k, const char *v, } return 0; } + if (!strcmp(k, "status.deserializewait")) { + if (!v || !*v) + implicit_deserialize_wait = DESERIALIZE_WAIT__UNSET; + else + implicit_deserialize_wait = parse_dw(v); + return 0; + } if (!strcmp(k, "status.showuntrackedfiles")) { enum untracked_status_type u; @@ -1651,6 +1695,9 @@ struct repository *repo UNUSED) OPT_CALLBACK_F(0, "deserialize", NULL, N_("path"), N_("deserialize raw status data from file"), PARSE_OPT_OPTARG, opt_parse_deserialize), + OPT_CALLBACK_F(0, "deserialize-wait", NULL, + N_("fail|block|no"), N_("how to wait if status cache file is invalid"), + PARSE_OPT_OPTARG, opt_parse_deserialize_wait), OPT_SET_INT(0, "long", &status_format, N_("show status in long format (default)"), STATUS_FORMAT_LONG), @@ -1764,11 +1811,21 @@ struct repository *repo UNUSED) } if (try_deserialize) { + int result; + enum wt_status_deserialize_wait dw = implicit_deserialize_wait; + if (explicit_deserialize_wait != DESERIALIZE_WAIT__UNSET) + dw = explicit_deserialize_wait; + if (dw == DESERIALIZE_WAIT__UNSET) + dw = DESERIALIZE_WAIT__NO; + if (s.relative_paths) s.prefix = prefix; - if (wt_status_deserialize(&s, deserialize_path) == DESERIALIZE_OK) + result = wt_status_deserialize(&s, deserialize_path, dw); + if (result == DESERIALIZE_OK) return 0; + if (dw == DESERIALIZE_WAIT__FAIL) + die(_("Rejected status serialization cache")); /* deserialize failed, so force the initialization we skipped above. */ enable_fscache(1); diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index 361afca94835e0..edf15d7af45489 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -199,6 +199,58 @@ test_expect_success 'verify new --serialize=path mode' ' test_cmp expect output.2 ' +test_expect_success 'try deserialize-wait feature' ' + test_when_finished "rm -f serialized_status.dat dirt expect.* output.* trace.*" && + + git status --serialize=serialized_status.dat >output.1 && + + # make status cache stale by updating the mtime on the index. confirm that + # deserialize fails when requested. + sleep 1 && + touch .git/index && + test_must_fail git status --deserialize=serialized_status.dat --deserialize-wait=fail && + test_must_fail git -c status.deserializeWait=fail status --deserialize=serialized_status.dat && + + cat >expect.1 <<-\EOF && + ? expect.1 + ? output.1 + ? serialized_status.dat + ? untracked/ + ? untracked_1.txt + EOF + + # refresh the status cache. + git status --porcelain=v2 --serialize=serialized_status.dat >output.1 && + test_cmp expect.1 output.1 && + + # create some dirt. confirm deserialize used the existing status cache. + echo x >dirt && + git status --porcelain=v2 --deserialize=serialized_status.dat >output.2 && + test_cmp output.1 output.2 && + + # make the cache stale and try the timeout feature and wait upto + # 2 tenths of a second. confirm deserialize timed out and rejected + # the status cache and did a normal scan. + + cat >expect.2 <<-\EOF && + ? dirt + ? expect.1 + ? expect.2 + ? output.1 + ? output.2 + ? serialized_status.dat + ? trace.2 + ? untracked/ + ? untracked_1.txt + EOF + + sleep 1 && + touch .git/index && + GIT_TRACE_DESERIALIZE=1 git status --porcelain=v2 --deserialize=serialized_status.dat --deserialize-wait=2 >output.2 2>trace.2 && + test_cmp expect.2 output.2 && + grep "wait polled=2 result=1" trace.2 >trace.2g +' + test_expect_success 'merge conflicts' ' # create a merge conflict. diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index 93961a6bf9f20d..e18d3b9cf9cccc 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -62,7 +62,8 @@ static int my_validate_index(const char *path, const struct cache_time *mtime_re mtime_observed_on_disk.nsec = ST_MTIME_NSEC(st); if ((mtime_observed_on_disk.sec != mtime_reported->sec) || (mtime_observed_on_disk.nsec != mtime_reported->nsec)) { - trace_printf_key(&trace_deserialize, "index mtime changed [des %d.%d][obs %d.%d]", + trace_printf_key(&trace_deserialize, + "index mtime changed [des %d %d][obs %d %d]", mtime_reported->sec, mtime_reported->nsec, mtime_observed_on_disk.sec, mtime_observed_on_disk.nsec); return DESERIALIZE_ERR; @@ -552,6 +553,8 @@ static inline int my_strcmp_null(const char *a, const char *b) static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *des_s, int fd) { + memset(des_s, 0, sizeof(*des_s)); + /* * Check the path spec on the current command */ @@ -681,8 +684,101 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de return DESERIALIZE_OK; } +static struct cache_time deserialize_prev_mtime = { 0, 0 }; + +static int try_deserialize_read_from_file_1(const struct wt_status *cmd_s, + const char *path, + struct wt_status *des_s) +{ + struct stat st; + int result; + int fd; + + /* + * If we are spinning waiting for the status cache to become + * valid, skip re-reading it if the mtime has not changed + * since the last time we read it. + */ + if (lstat(path, &st)) { + trace_printf_key(&trace_deserialize, + "could not lstat '%s'", path); + return DESERIALIZE_ERR; + } + if ((uint32_t)st.st_mtime == deserialize_prev_mtime.sec && + ST_MTIME_NSEC(st) == deserialize_prev_mtime.nsec) { + trace_printf_key(&trace_deserialize, + "mtime has not changed '%s'", path); + return DESERIALIZE_ERR; + } + + fd = xopen(path, O_RDONLY); + if (fd == -1) { + trace_printf_key(&trace_deserialize, + "could not read '%s'", path); + return DESERIALIZE_ERR; + } + + deserialize_prev_mtime.sec = st.st_mtime; + deserialize_prev_mtime.nsec = ST_MTIME_NSEC(st); + + trace_printf_key(&trace_deserialize, + "reading serialization file (%d %d) '%s'", + deserialize_prev_mtime.sec, + deserialize_prev_mtime.nsec, + path); + + result = wt_deserialize_fd(cmd_s, des_s, fd); + close(fd); + + return result; +} + +static int try_deserialize_read_from_file(const struct wt_status *cmd_s, + const char *path, + enum wt_status_deserialize_wait dw, + struct wt_status *des_s) +{ + int k, limit; + int result = DESERIALIZE_ERR; + + /* + * For "fail" or "no", try exactly once to read the status cache. + * Return an error if the file is stale. + */ + if (dw == DESERIALIZE_WAIT__FAIL || dw == DESERIALIZE_WAIT__NO) + return try_deserialize_read_from_file_1(cmd_s, path, des_s); + + /* + * Wait for the status cache file to refresh. Wait duration can + * be in tenths of a second or unlimited. Poll every 100ms. + */ + if (dw == DESERIALIZE_WAIT__BLOCK) { + /* + * Convert "unlimited" to 1 day. + */ + limit = 10 * 60 * 60 * 24; + } else { + /* spin for dw tenths of a second */ + limit = dw; + } + for (k = 0; k < limit; k++) { + result = try_deserialize_read_from_file_1( + cmd_s, path, des_s); + + if (result == DESERIALIZE_OK) + break; + + sleep_millisec(100); + } + + trace_printf_key(&trace_deserialize, + "wait polled=%d result=%d '%s'", + k, result, path); + return result; +} + /* - * Read raw serialized status data from the given file + * Read raw serialized status data from the given file (or STDIN). * * Verify that the args specified in the current command * are compatible with the deserialized data (such as "-uno"). @@ -690,25 +786,26 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de * Copy display-related fields from the current command * into the deserialized data (so that the user can request * long or short as they please). + * + * Print status report using cached data. */ int wt_status_deserialize(const struct wt_status *cmd_s, - const char *path) + const char *path, + enum wt_status_deserialize_wait dw) { struct wt_status des_s; int result; struct string_list_item *change; if (path && *path && strcmp(path, "0")) { - int fd = xopen(path, O_RDONLY); - if (fd == -1) { - trace_printf_key(&trace_deserialize, "could not read '%s'", path); - return DESERIALIZE_ERR; - } - trace_printf_key(&trace_deserialize, "reading serialization file '%s'", path); - result = wt_deserialize_fd(cmd_s, &des_s, fd); - close(fd); + result = try_deserialize_read_from_file(cmd_s, path, dw, &des_s); } else { trace_printf_key(&trace_deserialize, "reading stdin"); + + /* + * Read status cache data from stdin. Ignore the deserialize-wait + * term, since we cannot read stdin multiple times. + */ result = wt_deserialize_fd(cmd_s, &des_s, 0); } diff --git a/wt-status.h b/wt-status.h index 83a6a27de5146f..c6b33bcb318bb7 100644 --- a/wt-status.h +++ b/wt-status.h @@ -220,6 +220,15 @@ struct wt_status_serialize_data - sizeof(struct wt_status_serialize_data_fixed)]; }; +enum wt_status_deserialize_wait +{ + DESERIALIZE_WAIT__UNSET = -3, + DESERIALIZE_WAIT__FAIL = -2, /* return error, do not fallback */ + DESERIALIZE_WAIT__BLOCK = -1, /* unlimited timeout */ + DESERIALIZE_WAIT__NO = 0, /* immediately fallback */ + /* any positive value is a timeout in tenths of a second */ +}; + /* * Serialize computed status scan results using "version 1" format * to the given file. @@ -234,7 +243,8 @@ void wt_status_serialize_v1(int fd, struct wt_status *s); * fields. */ int wt_status_deserialize(const struct wt_status *cmd_s, - const char *path); + const char *path, + enum wt_status_deserialize_wait dw); /* * A helper routine for serialize and deserialize to compute From afef9e942788d6f6118eb3f4169ff945dca4a573 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 25 Jun 2019 16:38:50 -0400 Subject: [PATCH 097/229] status: deserialize with -uno does not print correct hint With the "--untracked-files=complete" option status computes a superset of the untracked files. We use this when writing the status cache. If subsequent deserialize commands ask for either the complete set or one of the "no", "normal", or "all" subsets, it can still use the cache file because of filtering in the deserialize parser. When running status with the "-uno" option, the long format status would print a "(use -u to show untracked files)" hint. When deserializing with the "-uno" option and using a cache computed with "-ucomplete", the "nothing to commit, working tree clean" message would be printed instead of the hint. It was easy to miss because the correct hint message was printed if the cache was rejected for any reason (and status did the full fallback). The "struct wt_status des" structure was initialized with the content of the status cache (and thus defaulted to "complete"). This change sets "des.show_untracked_files" to the requested subset from the command-line or config. This allows the long format to print the hint. Signed-off-by: Jeff Hostetler --- t/t7522-serialized-status.sh | 55 ++++++++++++++++++++++++++++++++++++ wt-status-deserialize.c | 16 +++++++---- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index edf15d7af45489..b52a9b7fa2f520 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -345,4 +345,59 @@ test_expect_success 'renames' ' test_cmp output.1 output.2 ' +test_expect_success 'hint message when cached with u=complete' ' + git init -b main hint && + echo xxx >hint/xxx && + git -C hint add xxx && + git -C hint commit -m xxx && + + cat >expect.clean <expect.use_u <hint.output_normal && + test_cmp expect.clean hint.output_normal && + + git -C hint status --untracked-files=all >hint.output_all && + test_cmp expect.clean hint.output_all && + + git -C hint status --untracked-files=no >hint.output_no && + test_cmp expect.use_u hint.output_no && + + # Create long format output for "complete" and create status cache. + + git -C hint status --untracked-files=complete --ignored=matching --serialize=../hint.dat >hint.output_complete && + test_cmp expect.clean hint.output_complete && + + # Capture long format output using the status cache and verify + # that the output matches the non-cached version. There are 2 + # ways to specify untracked-files, so do them both. + + git -C hint status --deserialize=../hint.dat -unormal >hint.d1_normal && + test_cmp expect.clean hint.d1_normal && + git -C hint -c status.showuntrackedfiles=normal status --deserialize=../hint.dat >hint.d2_normal && + test_cmp expect.clean hint.d2_normal && + + git -C hint status --deserialize=../hint.dat -uall >hint.d1_all && + test_cmp expect.clean hint.d1_all && + git -C hint -c status.showuntrackedfiles=all status --deserialize=../hint.dat >hint.d2_all && + test_cmp expect.clean hint.d2_all && + + git -C hint status --deserialize=../hint.dat -uno >hint.d1_no && + test_cmp expect.use_u hint.d1_no && + git -C hint -c status.showuntrackedfiles=no status --deserialize=../hint.dat >hint.d2_no && + test_cmp expect.use_u hint.d2_no + +' + test_done diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index e18d3b9cf9cccc..08ec246a586682 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -424,20 +424,24 @@ static int wt_deserialize_v1_ignored_items(struct wt_status *s, } static int validate_untracked_files_arg(enum untracked_status_type cmd, - enum untracked_status_type des, + enum untracked_status_type *des, enum deserialize_parse_strategy *strategy) { *strategy = DESERIALIZE_STRATEGY_AS_IS; - if (cmd == des) { + if (cmd == *des) { *strategy = DESERIALIZE_STRATEGY_AS_IS; } else if (cmd == SHOW_NO_UNTRACKED_FILES) { *strategy = DESERIALIZE_STRATEGY_SKIP; - } else if (des == SHOW_COMPLETE_UNTRACKED_FILES) { - if (cmd == SHOW_ALL_UNTRACKED_FILES) + *des = cmd; + } else if (*des == SHOW_COMPLETE_UNTRACKED_FILES) { + if (cmd == SHOW_ALL_UNTRACKED_FILES) { *strategy = DESERIALIZE_STRATEGY_ALL; - else if (cmd == SHOW_NORMAL_UNTRACKED_FILES) + *des = cmd; + } else if (cmd == SHOW_NORMAL_UNTRACKED_FILES) { *strategy = DESERIALIZE_STRATEGY_NORMAL; + *des = cmd; + } } else { return DESERIALIZE_ERR; } @@ -479,7 +483,7 @@ static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, * We now have the header parsed. Look at the command args (as passed in), and see how to parse * the serialized data */ - if (validate_untracked_files_arg(cmd_s->show_untracked_files, s->show_untracked_files, &untracked_strategy)) { + if (validate_untracked_files_arg(cmd_s->show_untracked_files, &s->show_untracked_files, &untracked_strategy)) { trace_printf_key(&trace_deserialize, "reject: show_untracked_file: command: %d, serialized : %d", cmd_s->show_untracked_files, s->show_untracked_files); From f4f16b95f9837dcd9dffe8754230369b02573dc1 Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Thu, 21 Nov 2019 12:01:04 -0700 Subject: [PATCH 098/229] fsmonitor: check CE_FSMONITOR_VALID in ce_uptodate When using fsmonitor the CE_FSMONITOR_VALID flag should be checked when wanting to know if the entry has been updated. If the flag is set the entry should be considered up to date and the same as if the CE_UPTODATE is set. In order to trust the CE_FSMONITOR_VALID flag, the fsmonitor data needs to be refreshed when the fsmonitor bitmap is applied to the index in tweak_fsmonitor. Since the fsmonitor data is kept up to date for every command, some tests needed to be updated to take that into account. istate->untracked->use_fsmonitor was set in tweak_fsmonitor when the fsmonitor bitmap data was loaded and is now in refresh_fsmonitor since that is being called in tweak_fsmonitor. refresh_fsmonitor will only be called once and any other callers should be setting it when refreshing the fsmonitor data so that code can use the fsmonitor data when checking untracked files. When writing the index, fsmonitor_last_update is used to determine if the fsmonitor bitmap should be created and the extension data written to the index. When running through unpack-trees this is not copied to the result index. This makes the next time a git command is ran do all the work of lstating all files to determine what is clean since all entries in the index are marked as dirty since there wasn't any fsmonitor data saved in the index extension. Copying the fsmonitor_last_update to the result index will cause the extension data for fsmonitor to be in the index for the next git command to use. Signed-off-by: Kevin Willford --- read-cache-ll.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read-cache-ll.h b/read-cache-ll.h index 71b49d9af48a9d..84092540a7830b 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -118,7 +118,7 @@ static inline unsigned create_ce_flags(unsigned stage) #define ce_namelen(ce) ((ce)->ce_namelen) #define ce_size(ce) cache_entry_size(ce_namelen(ce)) #define ce_stage(ce) ((CE_STAGEMASK & (ce)->ce_flags) >> CE_STAGESHIFT) -#define ce_uptodate(ce) ((ce)->ce_flags & CE_UPTODATE) +#define ce_uptodate(ce) (((ce)->ce_flags & CE_UPTODATE) || ((ce)->ce_flags & CE_FSMONITOR_VALID)) #define ce_skip_worktree(ce) ((ce)->ce_flags & CE_SKIP_WORKTREE) #define ce_mark_uptodate(ce) ((ce)->ce_flags |= CE_UPTODATE) #define ce_intent_to_add(ce) ((ce)->ce_flags & CE_INTENT_TO_ADD) From 10b3cecdc743777d2538bc400241be379bf1d83a Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 26 Sep 2018 12:29:26 -0400 Subject: [PATCH 099/229] gvfs:trace2:data: add trace2 tracing around read_object_process Add trace2 region around read_object_process to collect time spent waiting for missing objects to be dynamically fetched. Signed-off-by: Jeff Hostetler --- odb.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/odb.c b/odb.c index 3afecee47d9e71..d20e62e8393fa7 100644 --- a/odb.c +++ b/odb.c @@ -29,6 +29,7 @@ #include "submodule.h" #include "trace2.h" #include "trace.h" +#include "trace2.h" #include "write-or-die.h" KHASH_INIT(odb_path_map, const char * /* key: odb_path */, @@ -651,6 +652,8 @@ int read_object_process(struct repository *r, const struct object_id *oid) start = getnanotime(); + trace2_region_enter("subprocess", "read_object",r); + if (!subprocess_map_initialized) { subprocess_map_initialized = 1; hashmap_init(&subprocess_map, (hashmap_cmp_fn)cmd2process_cmp, @@ -667,13 +670,16 @@ int read_object_process(struct repository *r, const struct object_id *oid) if (subprocess_start(&subprocess_map, &entry->subprocess, cmd, start_read_object_fn)) { free(entry); - return -1; + err = -1; + goto leave_region; } } process = &entry->subprocess.process; - if (!(CAP_GET & entry->supported_capabilities)) - return -1; + if (!(CAP_GET & entry->supported_capabilities)) { + err = -1; + goto leave_region; + } sigchain_push(SIGPIPE, SIG_IGN); @@ -722,6 +728,10 @@ int read_object_process(struct repository *r, const struct object_id *oid) trace_performance_since(start, "read_object_process"); +leave_region: + trace2_region_leave_printf("subprocess", "read_object", r, + "result %d", err); + strbuf_release(&status); return err; } From 766798bc9fbe7d18a321247a403d2671f05e1fdf Mon Sep 17 00:00:00 2001 From: Kevin Willford Date: Thu, 21 Nov 2019 09:24:36 -0700 Subject: [PATCH 100/229] fsmonitor: add script for debugging and update script for tests The fsmonitor script that can be used for running all the git tests using watchman was causing some of the tests to fail because it wrote to stderr and created some files for debugging purposes. Add a new debug script to use with debugging and modify the other script to remove the code that would cause tests to fail. Signed-off-by: Kevin Willford --- t/t7519/fsmonitor-watchman | 22 +----- t/t7519/fsmonitor-watchman-debug | 128 +++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 21 deletions(-) create mode 100755 t/t7519/fsmonitor-watchman-debug diff --git a/t/t7519/fsmonitor-watchman b/t/t7519/fsmonitor-watchman index 264b9daf834ec8..6461f625f64181 100755 --- a/t/t7519/fsmonitor-watchman +++ b/t/t7519/fsmonitor-watchman @@ -17,7 +17,6 @@ use IPC::Open2; # 'git config core.fsmonitor .git/hooks/query-watchman' # my ($version, $time) = @ARGV; -#print STDERR "$0 $version $time\n"; # Check the hook interface version @@ -44,7 +43,7 @@ launch_watchman(); sub launch_watchman { - my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j') + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') or die "open2() failed: $!\n" . "Falling back to scanning...\n"; @@ -62,19 +61,11 @@ sub launch_watchman { "fields": ["name"] }] END - - open (my $fh, ">", ".git/watchman-query.json"); - print $fh $query; - close $fh; print CHLD_IN $query; close CHLD_IN; my $response = do {local $/; }; - open ($fh, ">", ".git/watchman-response.json"); - print $fh $response; - close $fh; - die "Watchman: command returned no output.\n" . "Falling back to scanning...\n" if $response eq ""; die "Watchman: command returned invalid output: $response\n" . @@ -93,7 +84,6 @@ sub launch_watchman { my $o = $json_pkg->new->utf8->decode($response); if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { - print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; $retry--; qx/watchman watch "$git_work_tree"/; die "Failed to make watchman watch '$git_work_tree'.\n" . @@ -103,11 +93,6 @@ sub launch_watchman { # return the fast "everything is dirty" flag to git and do the # Watchman query just to get it over with now so we won't pay # the cost in git to look up each individual file. - - open ($fh, ">", ".git/watchman-output.out"); - print "/\0"; - close $fh; - print "/\0"; eval { launch_watchman() }; exit 0; @@ -116,11 +101,6 @@ sub launch_watchman { die "Watchman: $o->{error}.\n" . "Falling back to scanning...\n" if $o->{error}; - open ($fh, ">", ".git/watchman-output.out"); - binmode $fh, ":utf8"; - print $fh @{$o->{files}}; - close $fh; - binmode STDOUT, ":utf8"; local $, = "\0"; print @{$o->{files}}; diff --git a/t/t7519/fsmonitor-watchman-debug b/t/t7519/fsmonitor-watchman-debug new file mode 100755 index 00000000000000..d8e7a1e5ba85c0 --- /dev/null +++ b/t/t7519/fsmonitor-watchman-debug @@ -0,0 +1,128 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 1) and a time in nanoseconds +# formatted as a string and outputs to stdout all files that have been +# modified since the given time. Paths must be relative to the root of +# the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $time) = @ARGV; +#print STDERR "$0 $version $time\n"; + +# Check the hook interface version + +if ($version == 1) { + # convert nanoseconds to seconds + # subtract one second to make sure watchman will return all changes + $time = int ($time / 1000000000) - 1; +} else { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree; +if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $git_work_tree = Win32::GetCwd(); + $git_work_tree =~ tr/\\/\//; +} else { + require Cwd; + $git_work_tree = Cwd::cwd(); +} + +my $retry = 1; + +launch_watchman(); + +sub launch_watchman { + + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $time but were not transient (ie created after + # $time but no longer exist). + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. + + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $time, + "fields": ["name"] + }] + END + + open (my $fh, ">", ".git/watchman-query.json"); + print $fh $query; + close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + open ($fh, ">", ".git/watchman-response.json"); + print $fh $response; + close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + my $json_pkg; + eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; + } or do { + require JSON::PP; + $json_pkg = "JSON::PP"; + }; + + my $o = $json_pkg->new->utf8->decode($response); + + if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { + print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; + $retry--; + qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + + open ($fh, ">", ".git/watchman-output.out"); + print "/\0"; + close $fh; + + print "/\0"; + eval { launch_watchman() }; + exit 0; + } + + die "Watchman: $o->{error}.\n" . + "Falling back to scanning...\n" if $o->{error}; + + open ($fh, ">", ".git/watchman-output.out"); + binmode $fh, ":utf8"; + print $fh @{$o->{files}}; + close $fh; + + binmode STDOUT, ":utf8"; + local $, = "\0"; + print @{$o->{files}}; +} From 69e430dac5c304a2c92318f2a6fdd43e3ec4ae9c Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 26 Sep 2018 11:21:22 -0400 Subject: [PATCH 101/229] gvfs:trace2:data: status deserialization information Add trace2 region and data events describing attempts to deserialize status data using a status cache. A category:status, label:deserialize region is pushed around the deserialize code. Deserialization results when reading from a file are: category:status, path = category:status, polled = category:status, result = "ok" | "reject" When reading from STDIN are: category:status, path = "STDIN" category:status, result = "ok" | "reject" Status will fallback and run a normal status scan when a "reject" is reported (unless "--deserialize-wait=fail"). If "ok" is reported, status was able to use the status cache and avoid scanning the workdir. Additionally, a cmd_mode is emitted for each step: collection, deserialization, and serialization. For example, if deserialization is attempted and fails and status falls back to actually computing the status, a cmd_mode message containing "deserialize" is issued and then a cmd_mode for "collect" is issued. Also, if deserialization fails, a data message containing the rejection reason is emitted. Signed-off-by: Jeff Hostetler --- builtin/commit.c | 19 +++++++++++- wt-status-deserialize.c | 67 ++++++++++++++++++++++++++++++++++++++--- wt-status.h | 2 ++ 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 63caf2ab310033..d427f4b55e89a9 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -170,6 +170,7 @@ static int opt_parse_porcelain(const struct option *opt, const char *arg, int un static int do_serialize = 0; static char *serialize_path = NULL; +static int reject_implicit = 0; static int do_implicit_deserialize = 0; static int do_explicit_deserialize = 0; static char *deserialize_path = NULL; @@ -233,7 +234,7 @@ static int opt_parse_deserialize(const struct option *opt UNUSED, const char *ar } if (!deserialize_path || !*deserialize_path) do_explicit_deserialize = 1; /* read stdin */ - else if (access(deserialize_path, R_OK) == 0) + else if (wt_status_deserialize_access(deserialize_path, R_OK) == 0) do_explicit_deserialize = 1; /* can read from this file */ else { /* @@ -1631,6 +1632,8 @@ static int git_status_config(const char *k, const char *v, if (v && *v && access(v, R_OK) == 0) { do_implicit_deserialize = 1; deserialize_path = xstrdup(v); + } else { + reject_implicit = 1; } return 0; } @@ -1801,6 +1804,17 @@ struct repository *repo UNUSED) if (try_deserialize) goto skip_init; + /* + * If we implicitly received a status cache pathname from the config + * and the file does not exist, we silently reject it and do the normal + * status "collect". Fake up some trace2 messages to reflect this and + * assist post-processors know this case is different. + */ + if (!do_serialize && reject_implicit) { + trace2_cmd_mode("implicit-deserialize"); + trace2_data_string("status", the_repository, "deserialize/reject", + "status-cache/access"); + } enable_fscache(0); if (status_format != STATUS_FORMAT_PORCELAIN && @@ -1844,6 +1858,7 @@ struct repository *repo UNUSED) if (s.relative_paths) s.prefix = prefix; + trace2_cmd_mode("deserialize"); result = wt_status_deserialize(&s, deserialize_path, dw); if (result == DESERIALIZE_OK) return 0; @@ -1861,6 +1876,7 @@ struct repository *repo UNUSED) fd = -1; } + trace2_cmd_mode("collect"); wt_status_collect(&s); if (0 <= fd) @@ -1875,6 +1891,7 @@ struct repository *repo UNUSED) if (fd_serialize < 0) die_errno(_("could not serialize to '%s'"), serialize_path); + trace2_cmd_mode("serialize"); wt_status_serialize_v1(fd_serialize, &s); close(fd_serialize); } diff --git a/wt-status-deserialize.c b/wt-status-deserialize.c index 08ec246a586682..15001603599efe 100644 --- a/wt-status-deserialize.c +++ b/wt-status-deserialize.c @@ -9,6 +9,23 @@ #include "statinfo.h" #include "hex.h" #include "path.h" +#include "trace2.h" + +static void set_deserialize_reject_reason(const char *reason) +{ + trace2_data_string("status", the_repository, "deserialize/reject", + reason); +} + +int wt_status_deserialize_access(const char *path, int mode) +{ + int a = access(path, mode); + + if (a != 0) + set_deserialize_reject_reason("status-cache/access"); + + return a; +} static struct trace_key trace_deserialize = TRACE_KEY_INIT(DESERIALIZE); @@ -55,6 +72,7 @@ static int my_validate_index(const char *path, const struct cache_time *mtime_re struct cache_time mtime_observed_on_disk; if (lstat(path, &st)) { + set_deserialize_reject_reason("index/not-found"); trace_printf_key(&trace_deserialize, "could not stat index"); return DESERIALIZE_ERR; } @@ -62,6 +80,7 @@ static int my_validate_index(const char *path, const struct cache_time *mtime_re mtime_observed_on_disk.nsec = ST_MTIME_NSEC(st); if ((mtime_observed_on_disk.sec != mtime_reported->sec) || (mtime_observed_on_disk.nsec != mtime_reported->nsec)) { + set_deserialize_reject_reason("index/mtime-changed"); trace_printf_key(&trace_deserialize, "index mtime changed [des %d %d][obs %d %d]", mtime_reported->sec, mtime_reported->nsec, @@ -87,10 +106,12 @@ static int my_validate_excludes(const char *path, const char *key, const char *l r = (strcmp(line, sb.buf) ? DESERIALIZE_ERR : DESERIALIZE_OK); - if (r == DESERIALIZE_ERR) + if (r == DESERIALIZE_ERR) { + set_deserialize_reject_reason("excludes/changed"); trace_printf_key(&trace_deserialize, "%s changed [cached '%s'][observed '%s']", key, line, sb.buf); + } strbuf_release(&sb); return r; @@ -146,6 +167,7 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) &index_mtime.sec, &index_mtime.nsec); if (nr_fields != 2) { + set_deserialize_reject_reason("v1-header/invalid-index-mtime"); trace_printf_key(&trace_deserialize, "invalid index_mtime (%d) '%s'", nr_fields, line); return DESERIALIZE_ERR; @@ -229,6 +251,7 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) /* status_format */ if (skip_prefix(line, "sha1_commit ", &arg)) { if (get_oid_hex(arg, &s->oid_commit)) { + set_deserialize_reject_reason("v1-header/invalid-commit-sha"); trace_printf_key(&trace_deserialize, "invalid sha1_commit"); return DESERIALIZE_ERR; } @@ -244,19 +267,23 @@ static int wt_deserialize_v1_header(struct wt_status *s, int fd) } /* prefix */ + set_deserialize_reject_reason("v1-header/unexpected-line"); trace_printf_key(&trace_deserialize, "unexpected line '%s'", line); return DESERIALIZE_ERR; } if (!have_required_index_mtime) { + set_deserialize_reject_reason("v1-header/missing-index-mtime"); trace_printf_key(&trace_deserialize, "missing '%s'", "index_mtime"); return DESERIALIZE_ERR; } if (!have_required_core_excludes) { + set_deserialize_reject_reason("v1-header/missing-core-excludes"); trace_printf_key(&trace_deserialize, "missing '%s'", "core_excludes"); return DESERIALIZE_ERR; } if (!have_required_repo_excludes) { + set_deserialize_reject_reason("v1-header/missing-repo-excludes"); trace_printf_key(&trace_deserialize, "missing '%s'", "repo_excludes"); return DESERIALIZE_ERR; } @@ -343,6 +370,7 @@ static int wt_deserialize_v1_changed_items(const struct wt_status *cmd_s, * So we reject the status cache and let the fallback * code run. */ + set_deserialize_reject_reason("v1-data/unmerged"); trace_printf_key( &trace_deserialize, "reject: V2 format and umerged file: %s", @@ -484,6 +512,7 @@ static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, * the serialized data */ if (validate_untracked_files_arg(cmd_s->show_untracked_files, &s->show_untracked_files, &untracked_strategy)) { + set_deserialize_reject_reason("args/untracked-files"); trace_printf_key(&trace_deserialize, "reject: show_untracked_file: command: %d, serialized : %d", cmd_s->show_untracked_files, s->show_untracked_files); @@ -491,6 +520,7 @@ static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, } if (validate_ignored_files_arg(cmd_s->show_ignored_mode, s->show_ignored_mode, &ignored_strategy)) { + set_deserialize_reject_reason("args/ignored-mode"); trace_printf_key(&trace_deserialize, "reject: show_ignored_mode: command: %d, serialized: %d", cmd_s->show_ignored_mode, s->show_ignored_mode); @@ -524,6 +554,7 @@ static int wt_deserialize_v1(const struct wt_status *cmd_s, struct wt_status *s, return DESERIALIZE_ERR; continue; } + set_deserialize_reject_reason("v1-data/unexpected-line"); trace_printf_key(&trace_deserialize, "unexpected line '%s'", line); return DESERIALIZE_ERR; } @@ -543,6 +574,7 @@ static int wt_deserialize_parse(const struct wt_status *cmd_s, struct wt_status if (version == 1) return wt_deserialize_v1(cmd_s, s, fd); } + set_deserialize_reject_reason("status-cache/unsupported-version"); trace_printf_key(&trace_deserialize, "missing/unsupported version"); return DESERIALIZE_ERR; } @@ -563,6 +595,7 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de * Check the path spec on the current command */ if (cmd_s->pathspec.nr > 1) { + set_deserialize_reject_reason("args/multiple-pathspecs"); trace_printf_key(&trace_deserialize, "reject: multiple pathspecs"); return DESERIALIZE_ERR; } @@ -573,6 +606,7 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de */ if (cmd_s->pathspec.nr == 1 && my_strcmp_null(cmd_s->pathspec.items[0].match, "")) { + set_deserialize_reject_reason("args/root-pathspec"); trace_printf_key(&trace_deserialize, "reject: pathspec"); return DESERIALIZE_ERR; } @@ -595,20 +629,24 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de * or "--ignored" settings). */ if (cmd_s->is_initial != des_s->is_initial) { + set_deserialize_reject_reason("args/is-initial-changed"); trace_printf_key(&trace_deserialize, "reject: is_initial"); return DESERIALIZE_ERR; } if (my_strcmp_null(cmd_s->branch, des_s->branch)) { + set_deserialize_reject_reason("args/branch-changed"); trace_printf_key(&trace_deserialize, "reject: branch"); return DESERIALIZE_ERR; } if (my_strcmp_null(cmd_s->reference, des_s->reference)) { + set_deserialize_reject_reason("args/reference-changed"); trace_printf_key(&trace_deserialize, "reject: reference"); return DESERIALIZE_ERR; } /* verbose */ /* amend */ if (cmd_s->whence != des_s->whence) { + set_deserialize_reject_reason("args/whence-changed"); trace_printf_key(&trace_deserialize, "reject: whence"); return DESERIALIZE_ERR; } @@ -642,19 +680,23 @@ static int wt_deserialize_fd(const struct wt_status *cmd_s, struct wt_status *de /* hints */ /* ahead_behind_flags */ if (cmd_s->detect_rename != des_s->detect_rename) { + set_deserialize_reject_reason("args/detect-rename-changed"); trace_printf_key(&trace_deserialize, "reject: detect_rename"); return DESERIALIZE_ERR; } if (cmd_s->rename_score != des_s->rename_score) { + set_deserialize_reject_reason("args/rename-score-changed"); trace_printf_key(&trace_deserialize, "reject: rename_score"); return DESERIALIZE_ERR; } if (cmd_s->rename_limit != des_s->rename_limit) { + set_deserialize_reject_reason("args/rename-limit-changed"); trace_printf_key(&trace_deserialize, "reject: rename_limit"); return DESERIALIZE_ERR; } /* status_format */ if (!oideq(&cmd_s->oid_commit, &des_s->oid_commit)) { + set_deserialize_reject_reason("args/commit-changed"); trace_printf_key(&trace_deserialize, "reject: sha1_commit"); return DESERIALIZE_ERR; } @@ -742,15 +784,18 @@ static int try_deserialize_read_from_file(const struct wt_status *cmd_s, enum wt_status_deserialize_wait dw, struct wt_status *des_s) { - int k, limit; + int k = 0; + int limit; int result = DESERIALIZE_ERR; /* * For "fail" or "no", try exactly once to read the status cache. * Return an error if the file is stale. */ - if (dw == DESERIALIZE_WAIT__FAIL || dw == DESERIALIZE_WAIT__NO) - return try_deserialize_read_from_file_1(cmd_s, path, des_s); + if (dw == DESERIALIZE_WAIT__FAIL || dw == DESERIALIZE_WAIT__NO) { + result = try_deserialize_read_from_file_1(cmd_s, path, des_s); + goto done; + } /* * Wait for the status cache file to refresh. Wait duration can @@ -775,6 +820,12 @@ static int try_deserialize_read_from_file(const struct wt_status *cmd_s, sleep_millisec(100); } +done: + trace2_data_string("status", the_repository, "deserialize/path", path); + trace2_data_intmax("status", the_repository, "deserialize/polled", k); + trace2_data_string("status", the_repository, "deserialize/result", + ((result == DESERIALIZE_OK) ? "ok" : "reject")); + trace_printf_key(&trace_deserialize, "wait polled=%d result=%d '%s'", k, result, path); @@ -801,6 +852,8 @@ int wt_status_deserialize(const struct wt_status *cmd_s, int result; struct string_list_item *change; + trace2_region_enter("status", "deserialize", the_repository); + if (path && *path && strcmp(path, "0")) { result = try_deserialize_read_from_file(cmd_s, path, dw, &des_s); } else { @@ -811,8 +864,14 @@ int wt_status_deserialize(const struct wt_status *cmd_s, * term, since we cannot read stdin multiple times. */ result = wt_deserialize_fd(cmd_s, &des_s, 0); + + trace2_data_string("status", the_repository, "deserialize/path", "STDIN"); + trace2_data_string("status", the_repository, "deserialize/result", + ((result == DESERIALIZE_OK) ? "ok" : "reject")); } + trace2_region_leave("status", "deserialize", the_repository); + if (result == DESERIALIZE_OK) { wt_status_get_state(cmd_s->repo, &des_s.state, des_s.branch && !strcmp(des_s.branch, "HEAD")); diff --git a/wt-status.h b/wt-status.h index c6b33bcb318bb7..95e7680ff21390 100644 --- a/wt-status.h +++ b/wt-status.h @@ -246,6 +246,8 @@ int wt_status_deserialize(const struct wt_status *cmd_s, const char *path, enum wt_status_deserialize_wait dw); +int wt_status_deserialize_access(const char *path, int mode); + /* * A helper routine for serialize and deserialize to compute * metadata for the user-global and repo-local excludes files. From 21b4089feb7f7428051fc77741fbe7b76f9a9075 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 10 Apr 2020 21:14:44 -0400 Subject: [PATCH 102/229] status: disable deserialize when verbose output requested. Disable deserialization when verbose output requested. Verbose mode causes Git to print diffs for modified files. This requires the index to be loaded to have the currently staged OID values. Without loading the index, verbose output make it look like everything was deleted. Signed-off-by: Jeff Hostetler --- builtin/commit.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/builtin/commit.c b/builtin/commit.c index ee31665c93aa61..9377b12abce880 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -43,6 +43,7 @@ #include "commit-graph.h" #include "pretty.h" #include "trailer.h" +#include "trace2.h" static const char * const builtin_commit_usage[] = { N_("git commit [-a | --interactive | --patch] [-s] [-v] [-u[]] [--amend]\n" @@ -1776,6 +1777,22 @@ struct repository *repo UNUSED) */ try_deserialize = (!do_serialize && (do_implicit_deserialize || do_explicit_deserialize)); + + /* + * Disable deserialize when verbose is set because it causes us to + * print diffs for each modified file, but that requires us to have + * the index loaded and we don't want to do that (at least not now for + * this seldom used feature). My fear is that would further tangle + * the merge conflict with upstream. + * + * TODO Reconsider this in the future. + */ + if (try_deserialize && verbose) { + trace2_data_string("status", the_repository, "deserialize/reject", + "args/verbose"); + try_deserialize = 0; + } + if (try_deserialize) goto skip_init; From 3148bd67f92d8bdda5d7ceb4276d73655869b357 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 7 Jan 2019 12:45:48 -0500 Subject: [PATCH 103/229] gvfs:trace2:data: status serialization Add trace information around status serialization. Signed-off-by: Jeff Hostetler --- wt-status-serialize.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wt-status-serialize.c b/wt-status-serialize.c index 6bbd4e575176f4..44bdb49d8eb85d 100644 --- a/wt-status-serialize.c +++ b/wt-status-serialize.c @@ -9,6 +9,7 @@ #include "trace.h" #include "read-cache-ll.h" #include "path.h" +#include "trace2.h" static struct trace_key trace_serialize = TRACE_KEY_INIT(SERIALIZE); @@ -305,6 +306,8 @@ void wt_status_serialize_v1(int fd, struct wt_status *s) struct string_list_item *iter; size_t k; + trace2_region_enter("status", "serialize", the_repository); + /* * version header must be first line. */ @@ -338,4 +341,6 @@ void wt_status_serialize_v1(int fd, struct wt_status *s) } packet_flush(fd); } + + trace2_region_leave("status", "serialize", the_repository); } From 77f39b5ddeda1dcd0e36b661096d0bbe9d5824c1 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 10 Apr 2020 21:18:41 -0400 Subject: [PATCH 104/229] t7524: add test for verbose status deserialzation Verify that `git status --deserialize=x -v` does not crash and generates the same output as a normal (scanning) status command. These issues are described in the previous 2 commits. Signed-off-by: Jeff Hostetler --- t/t7522-serialized-status.sh | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index b52a9b7fa2f520..6010fcd31635cd 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -400,4 +400,43 @@ EOF ' +test_expect_success 'ensure deserialize -v does not crash' ' + + git init -b main verbose_test && + touch verbose_test/a && + touch verbose_test/b && + touch verbose_test/c && + git -C verbose_test add a b c && + git -C verbose_test commit -m abc && + + echo green >>verbose_test/a && + git -C verbose_test add a && + echo red_1 >>verbose_test/b && + echo red_2 >verbose_test/dirt && + + git -C verbose_test status >output.ref && + git -C verbose_test status -v >output.ref_v && + + git -C verbose_test --no-optional-locks status --serialize=../verbose_test.dat >output.ser.long && + git -C verbose_test --no-optional-locks status --serialize=../verbose_test.dat_v -v >output.ser.long_v && + + # Verify that serialization does not affect the status output itself. + test_cmp output.ref output.ser.long && + test_cmp output.ref_v output.ser.long_v && + + GIT_TRACE2_PERF="$(pwd)"/verbose_test.log \ + git -C verbose_test status --deserialize=../verbose_test.dat >output.des.long && + + # Verify that normal deserialize was actually used and produces the same result. + test_cmp output.ser.long output.des.long && + grep -q "deserialize/result:ok" verbose_test.log && + + GIT_TRACE2_PERF="$(pwd)"/verbose_test.log_v \ + git -C verbose_test status --deserialize=../verbose_test.dat_v -v >output.des.long_v && + + # Verify that vebose mode produces the same result because verbose was rejected. + test_cmp output.ser.long_v output.des.long_v && + grep -q "deserialize/reject:args/verbose" verbose_test.log_v +' + test_done From b3c42cccbc827901d30437c5afb09bdb83004d05 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 19 Nov 2018 16:26:37 -0500 Subject: [PATCH 105/229] gvfs:trace2:data: add vfs stats Report virtual filesystem summary data. Signed-off-by: Jeff Hostetler --- virtualfilesystem.c | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/virtualfilesystem.c b/virtualfilesystem.c index 3f00800d86ef47..aff9cfd3eb8086 100644 --- a/virtualfilesystem.c +++ b/virtualfilesystem.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "environment.h" #include "gettext.h" +#include "trace2.h" #include "config.h" #include "dir.h" #include "hashmap.h" @@ -258,6 +259,11 @@ void apply_virtualfilesystem(struct index_state *istate) { char *buf, *entry; size_t i; + int nr_unknown = 0; + int nr_vfs_dirs = 0; + int nr_vfs_rows = 0; + int nr_bulk_skip = 0; + int nr_explicit_skip = 0; if (!repo_config_get_virtualfilesystem(istate->repo)) return; @@ -275,16 +281,21 @@ void apply_virtualfilesystem(struct index_state *istate) if (buf[i] == '\0') { ssize_t pos, len; + nr_vfs_rows++; + len = buf + i - entry; /* look for a directory wild card (ie "dir1/") */ if (buf[i - 1] == '/') { + nr_vfs_dirs++; if (ignore_case) adjust_dirname_case(istate, entry); pos = index_name_pos(istate, entry, len); if (pos < 0) { pos = -pos - 1; while ((size_t)pos < istate->cache_nr && !fspathncmp(istate->cache[pos]->name, entry, len)) { + if (istate->cache[pos]->ce_flags & CE_SKIP_WORKTREE) + nr_bulk_skip++; istate->cache[pos]->ce_flags &= ~CE_SKIP_WORKTREE; pos++; } @@ -292,18 +303,41 @@ void apply_virtualfilesystem(struct index_state *istate) } else { if (ignore_case) { struct cache_entry *ce = index_file_exists(istate, entry, len, ignore_case); - if (ce) + if (ce) { + if (ce->ce_flags & CE_SKIP_WORKTREE) + nr_explicit_skip++; ce->ce_flags &= ~CE_SKIP_WORKTREE; + } + else { + nr_unknown++; + } } else { int pos = index_name_pos(istate, entry, len); - if (pos >= 0) + if (pos >= 0) { + if (istate->cache[pos]->ce_flags & CE_SKIP_WORKTREE) + nr_explicit_skip++; istate->cache[pos]->ce_flags &= ~CE_SKIP_WORKTREE; + } + else { + nr_unknown++; + } } } entry += len + 1; } } + + if (nr_vfs_rows > 0) { + trace2_data_intmax("vfs", the_repository, "apply/tracked", nr_bulk_skip + nr_explicit_skip); + + trace2_data_intmax("vfs", the_repository, "apply/vfs_rows", nr_vfs_rows); + trace2_data_intmax("vfs", the_repository, "apply/vfs_dirs", nr_vfs_dirs); + + trace2_data_intmax("vfs", the_repository, "apply/nr_unknown", nr_unknown); + trace2_data_intmax("vfs", the_repository, "apply/nr_bulk_skip", nr_bulk_skip); + trace2_data_intmax("vfs", the_repository, "apply/nr_explicit_skip", nr_explicit_skip); + } } /* From 8142cd74a1dac73d5766f8b69dfd20092e5d1819 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 13 May 2020 17:38:50 -0400 Subject: [PATCH 106/229] deserialize-status: silently fallback if we cannot read cache file Teach Git to not throw a fatal error when an explicitly-specified status-cache file (`git status --deserialize=`) could not be found or opened for reading and silently fallback to a traditional scan. This matches the behavior when the status-cache file is implicitly given via a config setting. Note: the current version causes a test to start failing. Mark this as an expected result for now. Signed-off-by: Jeff Hostetler Signed-off-by: Derrick Stolee --- builtin/commit.c | 18 ++++++++++++------ t/t7522-serialized-status.sh | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index 9377b12abce880..63caf2ab310033 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -231,12 +231,18 @@ static int opt_parse_deserialize(const struct option *opt UNUSED, const char *ar free(deserialize_path); deserialize_path = xstrdup(arg); } - if (deserialize_path && *deserialize_path - && (access(deserialize_path, R_OK) != 0)) - die("cannot find serialization file '%s'", - deserialize_path); - - do_explicit_deserialize = 1; + if (!deserialize_path || !*deserialize_path) + do_explicit_deserialize = 1; /* read stdin */ + else if (access(deserialize_path, R_OK) == 0) + do_explicit_deserialize = 1; /* can read from this file */ + else { + /* + * otherwise, silently fallback to the normal + * collection scan + */ + do_implicit_deserialize = 0; + do_explicit_deserialize = 0; + } } return 0; diff --git a/t/t7522-serialized-status.sh b/t/t7522-serialized-status.sh index 6010fcd31635cd..230e1e24cfc1c4 100755 --- a/t/t7522-serialized-status.sh +++ b/t/t7522-serialized-status.sh @@ -439,4 +439,20 @@ test_expect_success 'ensure deserialize -v does not crash' ' grep -q "deserialize/reject:args/verbose" verbose_test.log_v ' +test_expect_success 'fallback when implicit' ' + git init -b main implicit_fallback_test && + git -C implicit_fallback_test -c status.deserializepath=foobar status +' + +test_expect_success 'fallback when explicit' ' + git init -b main explicit_fallback_test && + git -C explicit_fallback_test status --deserialize=foobar +' + +test_expect_success 'deserialize from stdin' ' + git init -b main stdin_test && + git -C stdin_test status --serialize >serialized_status.dat && + cat serialize_status.dat | git -C stdin_test status --deserialize +' + test_done From 47d83b930770c94c4fa343867a7643a38ce9e5d1 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Mon, 15 Apr 2019 13:39:43 -0700 Subject: [PATCH 107/229] trace2: refactor setting process starting time Create trace2_initialize_clock() and call from main() to capture process start time in isolation and before other sub-systems are ready. Signed-off-by: Jeff Hostetler Signed-off-by: Junio C Hamano --- compat/mingw.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index 6accd26f2a9808..485943a8f14d79 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -4330,6 +4330,8 @@ int wmain(int argc, const wchar_t **wargv) SetConsoleCtrlHandler(handle_ctrl_c, TRUE); + trace2_initialize_clock(); + maybe_redirect_std_handles(); adjust_symlink_flags(); fsync_object_files = 1; From 8553e358b763f1f58c1c2159ad76dcb917283d26 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 30 Apr 2019 14:12:51 -0400 Subject: [PATCH 108/229] trace2:gvfs:experiment: clear_ce_flags_1 Signed-off-by: Jeff Hostetler --- unpack-trees.c | 1 + 1 file changed, 1 insertion(+) diff --git a/unpack-trees.c b/unpack-trees.c index 98f352acca76f3..25c14a93718354 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1793,6 +1793,7 @@ static int clear_ce_flags(struct index_state *istate, xsnprintf(label, sizeof(label), "clear_ce_flags(0x%08lx,0x%08lx)", (unsigned long)select_mask, (unsigned long)clear_mask); trace2_region_enter("unpack_trees", label, the_repository); + rval = clear_ce_flags_1(istate, istate->cache, istate->cache_nr, From cc29e0ed9268d9e500c84e8d0153f1dbec63c745 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 30 Apr 2019 16:02:39 -0400 Subject: [PATCH 109/229] trace2:gvfs:experiment: report_tracking Signed-off-by: Jeff Hostetler --- builtin/checkout.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 72347addd208b8..be37145bcae5a1 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1061,8 +1061,11 @@ static void update_refs_for_switch(const struct checkout_opts *opts, strbuf_release(&msg); if (!opts->quiet && !opts->force_detach && - (new_branch_info->path || !strcmp(new_branch_info->name, "HEAD"))) + (new_branch_info->path || !strcmp(new_branch_info->name, "HEAD"))) { + trace2_region_enter("exp", "report_tracking", the_repository); report_tracking(new_branch_info); + trace2_region_leave("exp", "report_tracking", the_repository); + } } static int add_pending_uninteresting_ref(const char *refname, const char *referent UNUSED, From a33a33b5fd5557c71e6d4952bc3a88c7dee00c6b Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Thu, 3 Oct 2019 13:21:26 -0400 Subject: [PATCH 110/229] credential: set trace2_child_class for credential manager children Signed-off-by: Jeff Hostetler --- credential.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/credential.c b/credential.c index 2594c0c4229ba0..d1f31126bb5ff6 100644 --- a/credential.c +++ b/credential.c @@ -456,6 +456,8 @@ static int run_credential_helper(struct credential *c, else helper.no_stdout = 1; + helper.trace2_child_class = "cred"; + if (start_command(&helper) < 0) return -1; From ebd8ceec86a3e5468a08e861d9e86ca79a8e42f0 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Fri, 14 Jun 2019 12:38:31 -0400 Subject: [PATCH 111/229] trace2:gvfs:experiment: read_cache: annotate thread usage in read-cache Add trace2_thread_start() and trace2_thread_exit() events to the worker threads used to read the index. This gives per-thread perf data. These workers were introduced in: abb4bb83845 read-cache: load cache extensions on a worker thread 77ff1127a4c read-cache: load cache entries on worker threads Signed-off-by: Jeff Hostetler --- read-cache.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/read-cache.c b/read-cache.c index 9ff9184ebf00a1..bfbb6af75ad908 100644 --- a/read-cache.c +++ b/read-cache.c @@ -2026,6 +2026,17 @@ static void *load_index_extensions(void *_data) return NULL; } +static void *load_index_extensions_threadproc(void *_data) +{ + void *result; + + trace2_thread_start("load_index_extensions"); + result = load_index_extensions(_data); + trace2_thread_exit(); + + return result; +} + /* * A helper function that will load the specified range of cache entries * from the memory mapped file and add them to the given index. @@ -2102,12 +2113,17 @@ static void *load_cache_entries_thread(void *_data) struct load_cache_entries_thread_data *p = _data; int i; + trace2_thread_start("load_cache_entries"); + /* iterate across all ieot blocks assigned to this thread */ for (i = p->ieot_start; i < p->ieot_start + p->ieot_blocks; i++) { p->consumed += load_cache_entry_block(p->istate, p->ce_mem_pool, p->offset, p->ieot->entries[i].nr, p->mmap, p->ieot->entries[i].offset, NULL); p->offset += p->ieot->entries[i].nr; } + + trace2_thread_exit(); + return NULL; } @@ -2277,7 +2293,7 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) int err; p.src_offset = extension_offset; - err = pthread_create(&p.pthread, NULL, load_index_extensions, &p); + err = pthread_create(&p.pthread, NULL, load_index_extensions_threadproc, &p); if (err) die(_("unable to create load_index_extensions thread: %s"), strerror(err)); From eee1de946ebf9cd9fe839d6683aa6335e73f261f Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 18 Sep 2019 10:35:45 -0400 Subject: [PATCH 112/229] sub-process: do not borrow cmd pointer from caller Teach subprocess_start() to use a copy of the passed `cmd` string rather than borrowing the buffer from the caller. Some callers of subprocess_start() pass the value returned from find_hook() which points to a static buffer and therefore is only good until the next call to find_hook(). This could cause problems for the long-running background processes managed by sub-process.c where later calls to subprocess_find_entry() to get an existing process will fail. This could cause more than 1 long-running process to be created. TODO Need to confirm, but if only read_object_hook() uses TODO subprocess_start() in this manner, we could drop this TODO commit when we drop support for read_object_hook(). Signed-off-by: Jeff Hostetler --- sub-process.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sub-process.c b/sub-process.c index 83bf0a0e82e56d..0b3a9cafbc4d03 100644 --- a/sub-process.c +++ b/sub-process.c @@ -78,7 +78,12 @@ int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, co int err; struct child_process *process; - entry->cmd = cmd; + // BUGBUG most callers to subprocess_start() pass in "cmd" the value + // BUGBUG of find_hook() which returns a static buffer (that's only + // BUGBUG good until the next call to find_hook()). + // BUGFIX Defer assignment until we copy the string in our argv. + // entry->cmd = cmd; + process = &entry->process; child_process_init(process); @@ -90,6 +95,8 @@ int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, co process->clean_on_exit_handler = subprocess_exit_handler; process->trace2_child_class = "subprocess"; + entry->cmd = process->args.v[0]; + err = start_command(process); if (err) { error("cannot fork to run subprocess '%s'", cmd); From fb0c8e1d5650a297c832052899b564be4f8b0e71 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 9 Jul 2019 14:43:47 -0400 Subject: [PATCH 113/229] trace2:gvfs:experiment: read-cache: time read/write of cache-tree extension Add regions around code to read and write the cache-tree extension when the index is read or written. This is an experiment and may be dropped in future releases if we don't need it anymore. This experiment demonstrates that it takes more time to parse and deserialize the cache-tree extension than it does to read the cache-entries. Commits [1] and [2] spreads cache-entry reading across N-1 cores and dedicates a single core to simultaneously read the index extensions. Local testing (on my machine) shows that reading the cache-tree extension takes ~0.28 seconds. The 11 cache-entry threads take ~0.08 seconds. The main thread is blocked for 0.15 to 0.20 seconds waiting for the extension thread to finish. Let's use this commit to gather some telemetry and confirm this. My point is that improvements, such as index V5 which makes the cache entries smaller, may improve performance, but the gains may be limited because of this extension. And that we may need to look inside the cache-tree extension to truly improve do_read_index() performance. [1] abb4bb83845 read-cache: load cache extensions on a worker thread [2] 77ff1127a4c read-cache: load cache entries on worker threads Signed-off-by: Jeff Hostetler --- read-cache.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/read-cache.c b/read-cache.c index bfbb6af75ad908..4a2e920966e1d3 100644 --- a/read-cache.c +++ b/read-cache.c @@ -1736,7 +1736,10 @@ static int read_index_extension(struct index_state *istate, { switch (CACHE_EXT(ext)) { case CACHE_EXT_TREE: + trace2_region_enter("index", "read/extension/cache_tree", NULL); istate->cache_tree = cache_tree_read(data, sz); + trace2_data_intmax("index", NULL, "read/extension/cache_tree/bytes", (intmax_t)sz); + trace2_region_leave("index", "read/extension/cache_tree", NULL); break; case CACHE_EXT_RESOLVE_UNDO: istate->resolve_undo = resolve_undo_read(data, sz, the_hash_algo); @@ -3025,9 +3028,13 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile, !drop_cache_tree && istate->cache_tree) { strbuf_reset(&sb); + trace2_region_enter("index", "write/extension/cache_tree", NULL); cache_tree_write(&sb, istate->cache_tree); err = write_index_ext_header(f, eoie_c, CACHE_EXT_TREE, sb.len) < 0; hashwrite(f, sb.buf, sb.len); + trace2_data_intmax("index", NULL, "write/extension/cache_tree/bytes", (intmax_t)sb.len); + trace2_region_leave("index", "write/extension/cache_tree", NULL); + if (err) { ret = -1; goto out; From 27221938266730257ba8233ed3fa8149925cf7de Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 18 Sep 2019 10:45:58 -0400 Subject: [PATCH 114/229] sub-process: add subprocess_start_argv() Add function to start a subprocess with an argv. Signed-off-by: Jeff Hostetler --- sub-process.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ sub-process.h | 6 ++++++ 2 files changed, 53 insertions(+) diff --git a/sub-process.c b/sub-process.c index 0b3a9cafbc4d03..5d6110652d14c5 100644 --- a/sub-process.c +++ b/sub-process.c @@ -5,6 +5,7 @@ #include "sub-process.h" #include "sigchain.h" #include "pkt-line.h" +#include "quote.h" int cmd2process_cmp(const void *cmp_data UNUSED, const struct hashmap_entry *eptr, @@ -116,6 +117,52 @@ int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, co return 0; } +int subprocess_start_strvec(struct hashmap *hashmap, + struct subprocess_entry *entry, + int is_git_cmd, + const struct strvec *argv, + subprocess_start_fn startfn) +{ + int err; + size_t k; + struct child_process *process; + struct strbuf quoted = STRBUF_INIT; + + process = &entry->process; + + child_process_init(process); + for (k = 0; k < argv->nr; k++) + strvec_push(&process->args, argv->v[k]); + process->use_shell = 1; + process->in = -1; + process->out = -1; + process->git_cmd = is_git_cmd; + process->clean_on_exit = 1; + process->clean_on_exit_handler = subprocess_exit_handler; + process->trace2_child_class = "subprocess"; + + sq_quote_argv_pretty("ed, argv->v); + entry->cmd = strbuf_detach("ed, NULL); + + err = start_command(process); + if (err) { + error("cannot fork to run subprocess '%s'", entry->cmd); + return err; + } + + hashmap_entry_init(&entry->ent, strhash(entry->cmd)); + + err = startfn(entry); + if (err) { + error("initialization for subprocess '%s' failed", entry->cmd); + subprocess_stop(hashmap, entry); + return err; + } + + hashmap_add(hashmap, &entry->ent); + return 0; +} + static int handshake_version(struct child_process *process, const char *welcome_prefix, int *versions, int *chosen_version) diff --git a/sub-process.h b/sub-process.h index bfc3959a1b4894..3bd658c5dc8e3e 100644 --- a/sub-process.h +++ b/sub-process.h @@ -56,6 +56,12 @@ typedef int(*subprocess_start_fn)(struct subprocess_entry *entry); int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, const char *cmd, subprocess_start_fn startfn); +int subprocess_start_strvec(struct hashmap *hashmap, + struct subprocess_entry *entry, + int is_git_cmd, + const struct strvec *argv, + subprocess_start_fn startfn); + /* Kill a subprocess and remove it from the subprocess hashmap. */ void subprocess_stop(struct hashmap *hashmap, struct subprocess_entry *entry); From f8d7367a45121976632b530a7b6d2e0cc155a673 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 16 Jul 2019 09:09:53 -0400 Subject: [PATCH 115/229] trace2:gvfs:experiment: add region to apply_virtualfilesystem() Signed-off-by: Jeff Hostetler --- virtualfilesystem.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/virtualfilesystem.c b/virtualfilesystem.c index aff9cfd3eb8086..66117dc8447757 100644 --- a/virtualfilesystem.c +++ b/virtualfilesystem.c @@ -268,6 +268,8 @@ void apply_virtualfilesystem(struct index_state *istate) if (!repo_config_get_virtualfilesystem(istate->repo)) return; + trace2_region_enter("vfs", "apply", the_repository); + if (!virtual_filesystem_data.len) get_virtual_filesystem_data(istate->repo, &virtual_filesystem_data); @@ -338,6 +340,8 @@ void apply_virtualfilesystem(struct index_state *istate) trace2_data_intmax("vfs", the_repository, "apply/nr_bulk_skip", nr_bulk_skip); trace2_data_intmax("vfs", the_repository, "apply/nr_explicit_skip", nr_explicit_skip); } + + trace2_region_leave("vfs", "apply", the_repository); } /* From c62971cdaf3b632161d8ca0084c9bc5cf18c39bc Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 24 Sep 2019 14:31:10 -0400 Subject: [PATCH 116/229] sha1-file: add function to update existing loose object cache Create a function to add a new object to the loose object cache after the existing odb/xx/ directory was scanned. This will be used in a later commit to keep the loose object cache fresh after dynamically fetching an individual object and without requiring the odb/xx/ directory to be rescanned. Signed-off-by: Jeff Hostetler --- object-file.c | 7 +++++++ object-file.h | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/object-file.c b/object-file.c index 6723da7ce12cd8..198411ecd8f8b5 100644 --- a/object-file.c +++ b/object-file.c @@ -1846,6 +1846,13 @@ struct oidtree *odb_loose_cache(struct odb_source *source, return source->loose_objects_cache; } +void odb_loose_cache_add_new_oid(struct odb_source *source, + const struct object_id *oid) +{ + struct oidtree *cache = odb_loose_cache(source, oid); + append_loose_object(oid, NULL, cache); +} + void odb_clear_loose_cache(struct odb_source *source) { oidtree_clear(source->loose_objects_cache); diff --git a/object-file.h b/object-file.h index f9429d0bc002b8..88134f6f05bd03 100644 --- a/object-file.h +++ b/object-file.h @@ -33,6 +33,14 @@ struct odb_source; struct oidtree *odb_loose_cache(struct odb_source *source, const struct object_id *oid); +/* + * Add a new object to the loose object cache (possibly after the + * cache was populated). This might be used after dynamically + * fetching a missing object. + */ +void odb_loose_cache_add_new_oid(struct odb_source *source, + const struct object_id *oid); + /* Empty the loose object cache for the specified object directory. */ void odb_clear_loose_cache(struct odb_source *source); From 648e50fcf24d2df5eeb74324d2dcd13791cb0f08 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 16 Jul 2019 10:08:08 -0400 Subject: [PATCH 117/229] trace2:gvfs:experiment: add region around unpack_trees() Signed-off-by: Jeff Hostetler --- unpack-trees.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unpack-trees.c b/unpack-trees.c index 25c14a93718354..e59444e9473964 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1916,6 +1916,8 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options if (o->df_conflict_entry) BUG("o->df_conflict_entry is an output only field"); + trace2_region_enter("exp", "unpack_trees", NULL); + trace_performance_enter(); trace2_region_enter("unpack_trees", "unpack_trees", the_repository); @@ -2125,6 +2127,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options } trace2_region_leave("unpack_trees", "unpack_trees", the_repository); trace_performance_leave("unpack_trees"); + trace2_region_leave("exp", "unpack_trees", NULL); return ret; return_failed: From 6e7e322f0f120f1ff41013ab25cb31ff440f1b67 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Wed, 25 Sep 2019 13:36:54 -0400 Subject: [PATCH 118/229] packfile: add install_packed_git_and_mru() Create a function to install a new packfile into the packed-git list and add it to the head of the MRU list. This function will be used in a later commit to install packfiles created by dynamic object fetching. Signed-off-by: Jeff Hostetler --- packfile.c | 8 ++++++++ packfile.h | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/packfile.c b/packfile.c index 9fa6b8c97057ab..1a74bdb7c5cae2 100644 --- a/packfile.c +++ b/packfile.c @@ -792,6 +792,14 @@ void packfile_store_add_pack(struct packfile_store *store, hashmap_add(&store->map, &pack->packmap_ent); } +void packfile_store_add_pack_also_to_mru(struct repository *r, + struct packed_git *pack) +{ + packfile_store_add_pack(r->objects->packfiles, pack); + list_add(&pack->mru, + packfile_store_get_packs_mru(r->objects->packfiles)); +} + struct packed_git *packfile_store_load_pack(struct packfile_store *store, const char *idx_path, int local) { diff --git a/packfile.h b/packfile.h index 1b7e0ef74e11d1..a19587e57d1388 100644 --- a/packfile.h +++ b/packfile.h @@ -135,6 +135,13 @@ void packfile_store_reprepare(struct packfile_store *store); */ void packfile_store_add_pack(struct packfile_store *store, struct packed_git *pack); +/* + * Add the pack to the store so that contained objects become accessible via + * the store, and then mark it as most recently used. This moves ownership into + * the store. + */ +void packfile_store_add_pack_also_to_mru(struct repository *r, + struct packed_git *pack); /* * Load and iterate through all packs of the given repository. This helper From 8d59be59e842780f38dfc83438146013a40eeff4 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 16 Jul 2019 10:16:37 -0400 Subject: [PATCH 119/229] trace2:gvfs:experiment: add region to cache_tree_fully_valid() Signed-off-by: Jeff Hostetler --- cache-tree.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cache-tree.c b/cache-tree.c index ab0b248ab4b0a2..8d47c3fe18f84b 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -233,7 +233,7 @@ static void discard_unused_subtrees(struct cache_tree *it) } } -int cache_tree_fully_valid(struct cache_tree *it) +static int cache_tree_fully_valid_1(struct cache_tree *it) { int i; if (!it) @@ -243,7 +243,7 @@ int cache_tree_fully_valid(struct cache_tree *it) HAS_OBJECT_RECHECK_PACKED | HAS_OBJECT_FETCH_PROMISOR)) return 0; for (i = 0; i < it->subtree_nr; i++) { - if (!cache_tree_fully_valid(it->down[i]->cache_tree)) + if (!cache_tree_fully_valid_1(it->down[i]->cache_tree)) return 0; } return 1; @@ -254,6 +254,17 @@ static int must_check_existence(const struct cache_entry *ce) return !(repo_has_promisor_remote(the_repository) && ce_skip_worktree(ce)); } +int cache_tree_fully_valid(struct cache_tree *it) +{ + int result; + + trace2_region_enter("cache_tree", "fully_valid", NULL); + result = cache_tree_fully_valid_1(it); + trace2_region_leave("cache_tree", "fully_valid", NULL); + + return result; +} + static int update_one(struct cache_tree *it, struct cache_entry **cache, int entries, From ffc80f3944868b629b2a117512a2546f5fad9bb2 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 24 Sep 2019 15:51:16 -0400 Subject: [PATCH 120/229] index-pack: avoid immediate object fetch while parsing packfile Prevent packfile parsing from accidentally dynamically fetching each individual object found in the packfile. When index-pack parses the input packfile, it does a lookup in the ODB to test for conflicts/collisions. This can accidentally cause the object to be individually fetched when gvfs-helper (or read-object-hook or partial-clone) is enabled. Signed-off-by: Jeff Hostetler --- builtin/index-pack.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index b56ebaf74bc331..c2127b49d95a63 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -894,7 +894,7 @@ static void sha1_object(const void *data, struct object_entry *obj_entry, if (startup_info->have_repository) { read_lock(); collision_test_needed = odb_has_object(the_repository->objects, oid, - HAS_OBJECT_FETCH_PROMISOR); + OBJECT_INFO_FOR_PREFETCH); read_unlock(); } From d43bd298bb95e2e8ae560b796c804cc480a0bff7 Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 16 Jul 2019 10:40:56 -0400 Subject: [PATCH 121/229] trace2:gvfs:experiment: add unpack_entry() counter to unpack_trees() and report_tracking() Signed-off-by: Jeff Hostetler Signed-off-by: Johannes Schindelin --- builtin/checkout.c | 6 ++++++ packfile.c | 9 +++++++++ packfile.h | 5 +++++ unpack-trees.c | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/builtin/checkout.c b/builtin/checkout.c index be37145bcae5a1..db9c22870187a3 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -21,6 +21,7 @@ #include "object-file.h" #include "object-name.h" #include "odb.h" +#include "packfile.h" #include "parse-options.h" #include "path.h" #include "preload-index.h" @@ -1062,8 +1063,13 @@ static void update_refs_for_switch(const struct checkout_opts *opts, if (!opts->quiet && !opts->force_detach && (new_branch_info->path || !strcmp(new_branch_info->name, "HEAD"))) { + unsigned long nr_unpack_entry_at_start; + trace2_region_enter("exp", "report_tracking", the_repository); + nr_unpack_entry_at_start = get_nr_unpack_entry(); report_tracking(new_branch_info); + trace2_data_intmax("exp", NULL, "report_tracking/nr_unpack_entries", + (intmax_t)(get_nr_unpack_entry() - nr_unpack_entry_at_start)); trace2_region_leave("exp", "report_tracking", the_repository); } } diff --git a/packfile.c b/packfile.c index 1ae2b2fe1eda77..9fa6b8c97057ab 100644 --- a/packfile.c +++ b/packfile.c @@ -1670,6 +1670,13 @@ struct unpack_entry_stack_ent { unsigned long size; }; +static unsigned long g_nr_unpack_entry; + +unsigned long get_nr_unpack_entry(void) +{ + return g_nr_unpack_entry; +} + void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset, enum object_type *final_type, unsigned long *final_size) { @@ -1683,6 +1690,8 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset, int delta_stack_nr = 0, delta_stack_alloc = UNPACK_ENTRY_STACK_PREALLOC; int base_from_cache = 0; + g_nr_unpack_entry++; + prepare_repo_settings(p->repo); write_pack_access_log(p, obj_offset); diff --git a/packfile.h b/packfile.h index c9d0b93446b5f5..1b7e0ef74e11d1 100644 --- a/packfile.h +++ b/packfile.h @@ -388,4 +388,9 @@ int load_idx(const char *path, const unsigned int hashsz, void *idx_map, */ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *len); +/* + * Return the number of objects fetched from a packfile. + */ +unsigned long get_nr_unpack_entry(void); + #endif diff --git a/unpack-trees.c b/unpack-trees.c index e59444e9473964..6d73575a5cf81f 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -17,6 +17,7 @@ #include "tree-walk.h" #include "cache-tree.h" #include "unpack-trees.h" +#include "packfile.h" #include "progress.h" #include "refs.h" #include "attr.h" @@ -1903,6 +1904,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options struct pattern_list pl; int free_pattern_list = 0; struct dir_struct dir = DIR_INIT; + unsigned long nr_unpack_entry_at_start; if (o->reset == UNPACK_RESET_INVALID) BUG("o->reset had a value of 1; should be UNPACK_TREES_*_UNTRACKED"); @@ -1917,6 +1919,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options BUG("o->df_conflict_entry is an output only field"); trace2_region_enter("exp", "unpack_trees", NULL); + nr_unpack_entry_at_start = get_nr_unpack_entry(); trace_performance_enter(); trace2_region_enter("unpack_trees", "unpack_trees", the_repository); @@ -2127,6 +2130,8 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options } trace2_region_leave("unpack_trees", "unpack_trees", the_repository); trace_performance_leave("unpack_trees"); + trace2_data_intmax("unpack_trees", NULL, "unpack_trees/nr_unpack_entries", + (intmax_t)(get_nr_unpack_entry() - nr_unpack_entry_at_start)); trace2_region_leave("exp", "unpack_trees", NULL); return ret; From 81938b86125935f96e84f6a82f768f8b298e367b Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Tue, 13 Aug 2019 12:12:08 -0400 Subject: [PATCH 122/229] gvfs-helper: create tool to fetch objects using the GVFS Protocol Create gvfs-helper. This is a helper tool to use the GVFS Protocol REST API to fetch objects and configuration data from a GVFS cache-server or Git server. This tool uses libcurl to send object requests to either server. This tool creates loose objects and/or packfiles. Create gvfs-helper-client. This code resides within git proper and uses the sub-process API to manage gvfs-helper as a long-running background process. Signed-off-by: Jeff Hostetler Signed-off-by: Derrick Stolee --- .gitignore | 1 + Documentation/config.adoc | 2 + Documentation/config/core.adoc | 3 + Documentation/config/gvfs.adoc | 5 + Documentation/lint-manpages.sh | 1 + Makefile | 7 + config.c | 1 + contrib/buildsystems/CMakeLists.txt | 5 +- environment.c | 45 + environment.h | 3 + gvfs-helper-client.c | 372 +++++ gvfs-helper-client.h | 69 + gvfs-helper.c | 2324 +++++++++++++++++++++++++++ meson.build | 9 + odb.c | 33 +- promisor-remote.c | 13 +- t/helper/.gitignore | 1 + t/t1517-outside-repo.sh | 2 +- 18 files changed, 2892 insertions(+), 4 deletions(-) create mode 100644 Documentation/config/gvfs.adoc create mode 100644 gvfs-helper-client.c create mode 100644 gvfs-helper-client.h create mode 100644 gvfs-helper.c diff --git a/.gitignore b/.gitignore index 0482224716d834..954517a3a9d194 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ /git-gc /git-get-tar-commit-id /git-grep +/git-gvfs-helper /git-hash-object /git-help /git-hook diff --git a/Documentation/config.adoc b/Documentation/config.adoc index bd7187c7b48e4b..2b706ee4d2b006 100644 --- a/Documentation/config.adoc +++ b/Documentation/config.adoc @@ -449,6 +449,8 @@ include::config/gui.adoc[] include::config/guitool.adoc[] +include::config/gvfs.adoc[] + include::config/help.adoc[] include::config/http.adoc[] diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index e77d637182bc2c..48395904c80e99 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -811,6 +811,9 @@ core.gvfs:: flag just blocks them from occurring at all. -- +core.useGvfsHelper:: + TODO + core.sparseCheckout:: Enable "sparse checkout" feature. See linkgit:git-sparse-checkout[1] for more information. diff --git a/Documentation/config/gvfs.adoc b/Documentation/config/gvfs.adoc new file mode 100644 index 00000000000000..6ab221ded36c91 --- /dev/null +++ b/Documentation/config/gvfs.adoc @@ -0,0 +1,5 @@ +gvfs.cache-server:: + TODO + +gvfs.sharedcache:: + TODO diff --git a/Documentation/lint-manpages.sh b/Documentation/lint-manpages.sh index a0ea572382d8d9..1e0911e4e4be51 100755 --- a/Documentation/lint-manpages.sh +++ b/Documentation/lint-manpages.sh @@ -27,6 +27,7 @@ check_missing_docs () ( git-init-db) continue;; git-remote-*) continue;; git-stage) continue;; + git-gvfs-helper) continue;; git-legacy-*) continue;; git-?*--?* ) continue ;; esac diff --git a/Makefile b/Makefile index b99030489c4be8..f07b3026013186 100644 --- a/Makefile +++ b/Makefile @@ -1163,6 +1163,7 @@ LIB_OBJS += gpg-interface.o LIB_OBJS += graph.o LIB_OBJS += grep.o LIB_OBJS += gvfs.o +LIB_OBJS += gvfs-helper-client.o LIB_OBJS += hash-lookup.o LIB_OBJS += hash.o LIB_OBJS += hashmap.o @@ -1770,6 +1771,8 @@ endif endif BASIC_CFLAGS += $(CURL_CFLAGS) + PROGRAM_OBJS += gvfs-helper.o + REMOTE_CURL_PRIMARY = git-remote-http$X REMOTE_CURL_ALIASES = git-remote-https$X git-remote-ftp$X git-remote-ftps$X REMOTE_CURL_NAMES = $(REMOTE_CURL_PRIMARY) $(REMOTE_CURL_ALIASES) @@ -3026,6 +3029,10 @@ scalar$X: scalar.o GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) \ $(filter %.o,$^) $(LIBS) +git-gvfs-helper$X: gvfs-helper.o http.o GIT-LDFLAGS $(GITLIBS) $(LAZYLOAD_LIBCURL_OBJ) + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ + $(CURL_LIBCURL) $(EXPAT_LIBEXPAT) $(LIBS) + $(LIB_FILE): $(LIB_OBJS) $(QUIET_AR)$(RM) $@ && $(AR) $(ARFLAGS) $@ $^ diff --git a/config.c b/config.c index 629e6379aea000..78276139a6d808 100644 --- a/config.c +++ b/config.c @@ -37,6 +37,7 @@ #include "trace2.h" #include "wildmatch.h" #include "write-or-die.h" +#include "transport.h" struct config_source { struct config_source *prev; diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index 6443dfbac52663..580c84706fc4c5 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -635,7 +635,7 @@ if(NOT CURL_FOUND) add_compile_definitions(NO_CURL) message(WARNING "git-http-push and git-http-fetch will not be built") else() - list(APPEND PROGRAMS_BUILT git-http-fetch git-http-push git-imap-send git-remote-http) + list(APPEND PROGRAMS_BUILT git-http-fetch git-http-push git-imap-send git-remote-http git-gvfs-helper) if(CURL_VERSION_STRING VERSION_GREATER_EQUAL 7.34.0) add_compile_definitions(USE_CURL_FOR_IMAP_SEND) endif() @@ -811,6 +811,9 @@ if(CURL_FOUND) add_executable(git-http-push ${CMAKE_SOURCE_DIR}/http-push.c) target_link_libraries(git-http-push http_obj common-main ${CURL_LIBRARIES} ${EXPAT_LIBRARIES}) endif() + + add_executable(git-gvfs-helper ${CMAKE_SOURCE_DIR}/gvfs-helper.c) + target_link_libraries(git-gvfs-helper http_obj common-main ${CURL_LIBRARIES} ) endif() parse_makefile_for_executables(git_builtin_extra "BUILT_INS") diff --git a/environment.c b/environment.c index 721574f0dd9440..6a09261c416018 100644 --- a/environment.c +++ b/environment.c @@ -34,6 +34,7 @@ #include "quote.h" #include "chdir-notify.h" #include "setup.h" +#include "transport.h" #include "ws.h" #include "write-or-die.h" @@ -128,6 +129,9 @@ int protect_hfs = PROTECT_HFS_DEFAULT; #define PROTECT_NTFS_DEFAULT 1 #endif int protect_ntfs = PROTECT_NTFS_DEFAULT; +int core_use_gvfs_helper; +char *gvfs_cache_server_url; +const char *gvfs_shared_cache_pathname; /* * The character that begins a commented line in user-editable file @@ -558,6 +562,11 @@ int git_default_core_config(const char *var, const char *value, return 0; } + if (!strcmp(var, "core.usegvfshelper")) { + core_use_gvfs_helper = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "core.sparsecheckout")) { /* virtual file system relies on the sparse checkout logic so force it on */ if (core_virtualfilesystem) @@ -704,6 +713,39 @@ static int git_default_mailmap_config(const char *var, const char *value) return 0; } +static int git_default_gvfs_config(const char *var, const char *value) +{ + if (!strcmp(var, "gvfs.cache-server")) { + char *v2 = NULL; + + if (!git_config_string(&v2, var, value) && v2 && *v2) { + free(gvfs_cache_server_url); + gvfs_cache_server_url = transport_anonymize_url(v2); + } + free(v2); + return 0; + } + + if (!strcmp(var, "gvfs.sharedcache") && value && *value) { + struct strbuf buf = STRBUF_INIT; + strbuf_addstr(&buf, value); + if (strbuf_normalize_path(&buf) < 0) { + /* + * Pretend it wasn't set. This will cause us to + * fallback to ".git/objects" effectively. + */ + strbuf_release(&buf); + return 0; + } + strbuf_trim_trailing_dir_sep(&buf); + + gvfs_shared_cache_pathname = strbuf_detach(&buf, NULL); + return 0; + } + + return 0; +} + static int git_default_attr_config(const char *var, const char *value) { if (!strcmp(var, "attr.tree")) { @@ -771,6 +813,9 @@ int git_default_config(const char *var, const char *value, if (starts_with(var, "sparse.")) return git_default_sparse_config(var, value); + if (starts_with(var, "gvfs.")) + return git_default_gvfs_config(var, value); + /* Add other config variables here and to Documentation/config.adoc. */ return 0; } diff --git a/environment.h b/environment.h index ce510ac461b63c..d939da032b70bf 100644 --- a/environment.h +++ b/environment.h @@ -164,6 +164,9 @@ extern char *core_virtualfilesystem; extern int precomposed_unicode; extern int protect_hfs; extern int protect_ntfs; +extern int core_use_gvfs_helper; +extern char *gvfs_cache_server_url; +extern const char *gvfs_shared_cache_pathname; extern int core_apply_sparse_checkout; extern int core_sparse_checkout_cone; diff --git a/gvfs-helper-client.c b/gvfs-helper-client.c new file mode 100644 index 00000000000000..a4d3db6bf269f0 --- /dev/null +++ b/gvfs-helper-client.c @@ -0,0 +1,372 @@ +#define USE_THE_REPOSITORY_VARIABLE +#include "git-compat-util.h" +#include "gvfs-helper-client.h" +#include "hex.h" +#include "object-file.h" +#include "object.h" +#include "oidset.h" +#include "packfile.h" +#include "pkt-line.h" +#include "quote.h" +#include "sigchain.h" +#include "strvec.h" +#include "sub-process.h" +#include "trace2.h" + +static struct oidset gh_client__oidset_queued = OIDSET_INIT; +static unsigned long gh_client__oidset_count; +static int gh_client__includes_immediate; + +struct gh_server__process { + struct subprocess_entry subprocess; /* must be first */ + unsigned int supported_capabilities; +}; + +static int gh_server__subprocess_map_initialized; +static struct hashmap gh_server__subprocess_map; +static struct odb_source *gh_client__chosen_odb; + +#define CAP_GET (1u<<1) + +static int gh_client__start_fn(struct subprocess_entry *subprocess) +{ + static int versions[] = {1, 0}; + static struct subprocess_capability capabilities[] = { + { "get", CAP_GET }, + { NULL, 0 } + }; + + struct gh_server__process *entry = (struct gh_server__process *)subprocess; + + return subprocess_handshake(subprocess, "gvfs-helper", versions, + NULL, capabilities, + &entry->supported_capabilities); +} + +/* + * Send: + * + * get LF + * ( LF)* + * + * + */ +static int gh_client__get__send_command(struct child_process *process) +{ + struct oidset_iter iter; + struct object_id *oid; + int err; + + /* + * We assume that all of the packet_ routines call error() + * so that we don't have to. + */ + + err = packet_write_fmt_gently(process->in, "get\n"); + if (err) + return err; + + oidset_iter_init(&gh_client__oidset_queued, &iter); + while ((oid = oidset_iter_next(&iter))) { + err = packet_write_fmt_gently(process->in, "%s\n", + oid_to_hex(oid)); + if (err) + return err; + } + + err = packet_flush_gently(process->in); + if (err) + return err; + + return 0; +} + +/* + * Update the loose object cache to include the newly created + * object. + */ +static void gh_client__update_loose_cache(const char *line) +{ + const char *v1_oid; + struct object_id oid; + + if (!skip_prefix(line, "loose ", &v1_oid)) + BUG("update_loose_cache: invalid line '%s'", line); + + odb_loose_cache_add_new_oid(gh_client__chosen_odb, &oid); +} + +/* + * Update the packed-git list to include the newly created packfile. + */ +static void gh_client__update_packed_git(const char *line) +{ + struct strbuf path = STRBUF_INIT; + const char *v1_filename; + struct packed_git *p; + int is_local; + + if (!skip_prefix(line, "packfile ", &v1_filename)) + BUG("update_packed_git: invalid line '%s'", line); + + /* + * ODB[0] is the local .git/objects. All others are alternates. + */ + is_local = (gh_client__chosen_odb == the_repository->objects->sources); + + strbuf_addf(&path, "%s/pack/%s", + gh_client__chosen_odb->path, v1_filename); + strbuf_strip_suffix(&path, ".pack"); + strbuf_addstr(&path, ".idx"); + + p = add_packed_git(the_repository, path.buf, path.len, is_local); + if (p) + packfile_store_add_pack_also_to_mru(the_repository, p); + strbuf_release(&path); +} + +/* + * We expect: + * + * + * * + * + * + * + * Where: + * + * ::= odb SP LF + * + * ::= / + * + * ::= packfile SP LF + * + * ::= loose SP LF + * + * ::= ok LF + * / partial LF + * / error SP LF + * + * Note that `gvfs-helper` controls how/if it chunks the request when + * it talks to the cache-server and/or main Git server. So it is + * possible for us to receive many packfiles and/or loose objects *AND + * THEN* get a hard network error or a 404 on an individual object. + * + * If we get a partial result, we can let the caller try to continue + * -- for example, maybe an immediate request for a tree object was + * grouped with a queued request for a blob. The tree-walk *might* be + * able to continue and let the 404 blob be handled later. + */ +static int gh_client__get__receive_response( + struct child_process *process, + enum gh_client__created *p_ghc, + int *p_nr_loose, int *p_nr_packfile) +{ + enum gh_client__created ghc = GHC__CREATED__NOTHING; + const char *v1; + char *line; + int len; + int err = 0; + + while (1) { + /* + * Warning: packet_read_line_gently() calls die() + * despite the _gently moniker. + */ + len = packet_read_line_gently(process->out, NULL, &line); + if ((len < 0) || !line) + break; + + if (starts_with(line, "odb")) { + /* trust that this matches what we expect */ + } + + else if (starts_with(line, "packfile")) { + gh_client__update_packed_git(line); + ghc |= GHC__CREATED__PACKFILE; + *p_nr_packfile += 1; + } + + else if (starts_with(line, "loose")) { + gh_client__update_loose_cache(line); + ghc |= GHC__CREATED__LOOSE; + *p_nr_loose += 1; + } + + else if (starts_with(line, "ok")) + ; + else if (starts_with(line, "partial")) + ; + else if (skip_prefix(line, "error ", &v1)) { + error("gvfs-helper error: '%s'", v1); + err = -1; + } + } + + *p_ghc = ghc; + + return err; +} + +static void gh_client__choose_odb(void) +{ + if (gh_client__chosen_odb) + return; + + odb_prepare_alternates(the_repository->objects); + gh_client__chosen_odb = the_repository->objects->sources; +} + +static int gh_client__get(enum gh_client__created *p_ghc) +{ + struct gh_server__process *entry; + struct child_process *process; + struct strvec argv = STRVEC_INIT; + struct strbuf quoted = STRBUF_INIT; + int nr_loose = 0; + int nr_packfile = 0; + int err = 0; + + trace2_region_enter("gh-client", "get", the_repository); + + gh_client__choose_odb(); + + /* + * TODO decide what defaults we want. + */ + strvec_push(&argv, "gvfs-helper"); + strvec_push(&argv, "--fallback"); + strvec_push(&argv, "--cache-server=trust"); + strvec_pushf(&argv, "--shared-cache=%s", + gh_client__chosen_odb->path); + strvec_push(&argv, "server"); + + sq_quote_argv_pretty("ed, argv.v); + + if (!gh_server__subprocess_map_initialized) { + gh_server__subprocess_map_initialized = 1; + hashmap_init(&gh_server__subprocess_map, + (hashmap_cmp_fn)cmd2process_cmp, NULL, 0); + entry = NULL; + } else + entry = (struct gh_server__process *)subprocess_find_entry( + &gh_server__subprocess_map, quoted.buf); + + if (!entry) { + entry = xmalloc(sizeof(*entry)); + entry->supported_capabilities = 0; + + err = subprocess_start_strvec( + &gh_server__subprocess_map, &entry->subprocess, 1, + &argv, gh_client__start_fn); + if (err) { + free(entry); + goto leave_region; + } + } + + process = &entry->subprocess.process; + + if (!(CAP_GET & entry->supported_capabilities)) { + error("gvfs-helper: does not support GET"); + subprocess_stop(&gh_server__subprocess_map, + (struct subprocess_entry *)entry); + free(entry); + err = -1; + goto leave_region; + } + + sigchain_push(SIGPIPE, SIG_IGN); + + err = gh_client__get__send_command(process); + if (!err) + err = gh_client__get__receive_response(process, p_ghc, + &nr_loose, &nr_packfile); + + sigchain_pop(SIGPIPE); + + if (err) { + subprocess_stop(&gh_server__subprocess_map, + (struct subprocess_entry *)entry); + free(entry); + } + +leave_region: + strvec_clear(&argv); + strbuf_release("ed); + + trace2_data_intmax("gh-client", the_repository, + "get/immediate", gh_client__includes_immediate); + + trace2_data_intmax("gh-client", the_repository, + "get/nr_objects", gh_client__oidset_count); + + if (nr_loose) + trace2_data_intmax("gh-client", the_repository, + "get/nr_loose", nr_loose); + + if (nr_packfile) + trace2_data_intmax("gh-client", the_repository, + "get/nr_packfile", nr_packfile); + + if (err) + trace2_data_intmax("gh-client", the_repository, + "get/error", err); + + trace2_region_leave("gh-client", "get", the_repository); + + oidset_clear(&gh_client__oidset_queued); + gh_client__oidset_count = 0; + gh_client__includes_immediate = 0; + + return err; +} + +void gh_client__queue_oid(const struct object_id *oid) +{ + // TODO consider removing this trace2. it is useful for interactive + // TODO debugging, but may generate way too much noise for a data + // TODO event. + trace2_printf("gh_client__queue_oid: %s", oid_to_hex(oid)); + + if (!oidset_insert(&gh_client__oidset_queued, oid)) + gh_client__oidset_count++; +} + +/* + * This routine should actually take a "const struct oid_array *" + * rather than the component parts, but fetch_objects() uses + * this model (because of the call in sha1-file.c). + */ +void gh_client__queue_oid_array(const struct object_id *oids, int oid_nr) +{ + int k; + + for (k = 0; k < oid_nr; k++) + gh_client__queue_oid(&oids[k]); +} + +int gh_client__drain_queue(enum gh_client__created *p_ghc) +{ + *p_ghc = GHC__CREATED__NOTHING; + + if (!gh_client__oidset_count) + return 0; + + return gh_client__get(p_ghc); +} +int gh_client__get_immediate(const struct object_id *oid, + enum gh_client__created *p_ghc) +{ + gh_client__includes_immediate = 1; + + // TODO consider removing this trace2. it is useful for interactive + // TODO debugging, but may generate way too much noise for a data + // TODO event. + trace2_printf("gh_client__get_immediate: %s", oid_to_hex(oid)); + + if (!oidset_insert(&gh_client__oidset_queued, oid)) + gh_client__oidset_count++; + + return gh_client__drain_queue(p_ghc); +} diff --git a/gvfs-helper-client.h b/gvfs-helper-client.h new file mode 100644 index 00000000000000..c1e38fad75f841 --- /dev/null +++ b/gvfs-helper-client.h @@ -0,0 +1,69 @@ +#ifndef GVFS_HELPER_CLIENT_H +#define GVFS_HELPER_CLIENT_H + +struct repository; +struct commit; +struct object_id; + +enum gh_client__created { + /* + * The _get_ operation did not create anything. If doesn't + * matter if `gvfs-helper` had errors or not -- just that + * nothing was created. + */ + GHC__CREATED__NOTHING = 0, + + /* + * The _get_ operation created one or more packfiles. + */ + GHC__CREATED__PACKFILE = 1<<1, + + /* + * The _get_ operation created one or more loose objects. + * (Not necessarily the for the individual OID you requested.) + */ + GHC__CREATED__LOOSE = 1<<2, + + /* + * The _get_ operation created one or more packfilea *and* + * one or more loose objects. + */ + GHC__CREATED__PACKFILE_AND_LOOSE = (GHC__CREATED__PACKFILE | + GHC__CREATED__LOOSE), +}; + +/* + * Ask `gvfs-helper server` to immediately fetch a single object + * using "/gvfs/objects" GET semantics. + * + * A long-running background process is used to make subsequent + * requests more efficient. + * + * A loose object will be created in the shared-cache ODB and + * in-memory cache updated. + */ +int gh_client__get_immediate(const struct object_id *oid, + enum gh_client__created *p_ghc); + +/* + * Queue this OID for a future fetch using `gvfs-helper service`. + * It does not wait. + * + * Callers should not rely on the queued object being on disk until + * the queue has been drained. + */ +void gh_client__queue_oid(const struct object_id *oid); +void gh_client__queue_oid_array(const struct object_id *oids, int oid_nr); + +/* + * Ask `gvfs-helper server` to fetch the set of queued OIDs using + * "/gvfs/objects" POST semantics. + * + * A long-running background process is used to subsequent requests + * more efficient. + * + * One or more packfiles will be created in the shared-cache ODB. + */ +int gh_client__drain_queue(enum gh_client__created *p_ghc); + +#endif /* GVFS_HELPER_CLIENT_H */ diff --git a/gvfs-helper.c b/gvfs-helper.c new file mode 100644 index 00000000000000..20f7788fba1fe0 --- /dev/null +++ b/gvfs-helper.c @@ -0,0 +1,2324 @@ +// TODO Write a man page. Here are some notes for dogfooding. +// TODO +// +// Usage: git gvfs-helper [] [] +// +// : +// +// --remote= // defaults to "origin" +// +// --fallback // boolean. defaults to off +// +// When a fetch from the cache-server fails, automatically +// fallback to the main Git server. This option has no effect +// if no cache-server is defined. +// +// --cache-server= // defaults to "verify" +// +// verify := lookup the set of defined cache-servers using +// "gvfs/config" and confirm that the selected +// cache-server is well-known. Silently disable the +// cache-server if not. (See security notes later.) +// +// error := verify cache-server and abort if not well-known. +// +// trust := do not verify cache-server. just use it. +// +// disable := disable the cache-server and always use the main +// Git server. +// +// --shared-cache= +// +// A relative or absolute pathname to the ODB directory to store +// fetched objects. +// +// If this option is not specified, we default to the value +// in the "gvfs.sharedcache" config setting and then to the +// local ".git/objects" directory. +// +// : +// +// config +// +// Fetch the "gvfs/config" string from the main Git server. +// (The cache-server setting is ignored because cache-servers +// do not support this REST API.) +// +// get +// +// Fetch 1 or more objects. If a cache-server is configured, +// try it first. Optionally fallback to the main Git server. +// +// The set of objects is given on stdin and is assumed to be +// a list of , one per line. +// +// : +// +// --block-size= // defaults to "4000" +// +// Request objects from server in batches of at +// most n objects (not bytes). +// +// --depth= // defaults to "1" +// +// server +// +// Interactive/sub-process mode. Listen for a series of commands +// and data on stdin and return results on stdout. This command +// uses pkt-line format [1] and implements the long-running process +// protocol [2] to communicate with the foreground/parent process. +// +// : +// +// --block-size= // defaults to "4000" +// +// Request objects from server in batches of at +// most n objects (not bytes). +// +// --depth= // defaults to "1" +// +// Interactive verb: get +// +// Fetch 1 or more objects. If a cache-server is configured, +// try it first. Optionally fallback to the main Git server. +// Create 1 or more loose objects and/or packfiles in the +// requested shared-cache directory (given on the command +// line and which is reported at the beginning of the +// response). +// +// git> get +// git> +// git> +// git> ... +// git> +// git> 0000 +// +// git< odb +// git< loose | packfile +// git< loose | packfile +// gid< ... +// git< loose | packfile +// git< ok | partial | error +// git< 0000 +// +// [1] Documentation/technical/protocol-common.txt +// [2] Documentation/technical/long-running-process-protocol.txt +// [3] See GIT_TRACE_PACKET +// +// Example: +// +// $ git -c core.virtualizeobjects=false -c core.usegvfshelper=false +// rev-list --objects --no-walk --missing=print HEAD +// | grep "^?" +// | sed 's/^?//' +// | git gvfs-helper get-missing +// +// Note: In this example, we need to turn off "core.virtualizeobjects" and +// "core.usegvfshelper" when building the list of objects. This prevents +// rev-list (in oid_object_info_extended() from automatically fetching +// them with read-object-hook or "gvfs-helper server" sub-process (and +// defeating the whole purpose of this example). +// +////////////////////////////////////////////////////////////////// + +#define USE_THE_REPOSITORY_VARIABLE +#include "git-compat-util.h" +#include "git-curl-compat.h" +#include "environment.h" +#include "hex.h" +#include "setup.h" +#include "config.h" +#include "remote.h" +#include "connect.h" +#include "strbuf.h" +#include "walker.h" +#include "http.h" +#include "exec-cmd.h" +#include "run-command.h" +#include "pkt-line.h" +#include "string-list.h" +#include "sideband.h" +#include "strvec.h" +#include "credential.h" +#include "oid-array.h" +#include "send-pack.h" +#include "protocol.h" +#include "quote.h" +#include "transport.h" +#include "parse-options.h" +#include "odb.h" +#include "object-file.h" +#include "json-writer.h" +#include "tempfile.h" +#include "oidset.h" +#include "dir.h" +#include "url.h" +#include "abspath.h" +#include "progress.h" +#include "trace2.h" + +static const char * const main_usage[] = { + N_("git gvfs-helper [] config []"), + N_("git gvfs-helper [] get []"), + N_("git gvfs-helper [] server []"), + NULL +}; + +static const char *const get_usage[] = { + N_("git gvfs-helper [] get []"), + NULL +}; + +static const char *const server_usage[] = { + N_("git gvfs-helper [] server []"), + NULL +}; + +#define GH__DEFAULT_BLOCK_SIZE 4000 + +/* + * Our exit-codes. + */ +enum gh__error_code { + GH__ERROR_CODE__USAGE = -1, /* will be mapped to usage() */ + GH__ERROR_CODE__OK = 0, + GH__ERROR_CODE__ERROR = 1, /* unspecified */ +// GH__ERROR_CODE__CACHE_SERVER_NOT_FOUND = 2, + GH__ERROR_CODE__CURL_ERROR = 3, + GH__ERROR_CODE__HTTP_401 = 4, + GH__ERROR_CODE__HTTP_404 = 5, + GH__ERROR_CODE__HTTP_UNEXPECTED_CODE = 6, + GH__ERROR_CODE__UNEXPECTED_CONTENT_TYPE = 7, + GH__ERROR_CODE__COULD_NOT_CREATE_TEMPFILE = 8, + GH__ERROR_CODE__COULD_NOT_INSTALL_LOOSE = 9, + GH__ERROR_CODE__COULD_NOT_INSTALL_PACKFILE = 10, + GH__ERROR_CODE__SUBPROCESS_SYNTAX = 11, +}; + +enum gh__cache_server_mode { + /* verify URL. disable if unknown. */ + GH__CACHE_SERVER_MODE__VERIFY_DISABLE = 0, + /* verify URL. error if unknown. */ + GH__CACHE_SERVER_MODE__VERIFY_ERROR, + /* disable the cache-server, if defined */ + GH__CACHE_SERVER_MODE__DISABLE, + /* trust any cache-server */ + GH__CACHE_SERVER_MODE__TRUST_WITHOUT_VERIFY, +}; + +/* + * The set of command line, config, and environment variables + * that we use as input to decide how we should operate. + */ +static struct gh__cmd_opts { + const char *remote_name; + + int try_fallback; /* to git server if cache-server fails */ + int show_progress; + + int depth; + unsigned int block_size; + + enum gh__cache_server_mode cache_server_mode; +} gh__cmd_opts; + +/* + * The chosen global state derrived from the inputs in gh__cmd_opts. + */ +static struct gh__global { + struct remote *remote; + + struct credential main_creds; + struct credential cache_creds; + + const char *main_url; + const char *cache_server_url; + + struct strbuf buf_odb_path; + + int http_is_initialized; + int cache_server_is_initialized; /* did sub-command look for one */ + int main_creds_need_approval; /* try to only approve them once */ + +} gh__global; + +/* + * Stolen from http.c + */ +static CURLcode gh__curlinfo_strbuf(CURL *curl, CURLINFO info, struct strbuf *buf) +{ + char *ptr; + CURLcode ret; + + strbuf_reset(buf); + ret = curl_easy_getinfo(curl, info, &ptr); + if (!ret && ptr) + strbuf_addstr(buf, ptr); + return ret; +} + +enum gh__progress_state { + GH__PROGRESS_STATE__START = 0, + GH__PROGRESS_STATE__PHASE1, + GH__PROGRESS_STATE__PHASE2, + GH__PROGRESS_STATE__PHASE3, +}; + +/* + * Parameters to drive an HTTP request (with any necessary retries). + */ +struct gh__request_params { + int b_is_post; /* POST=1 or GET=0 */ + int b_write_to_file; /* write to file=1 or strbuf=0 */ + int b_no_cache_server; /* force main server only */ + + unsigned long object_count; /* number of objects being fetched */ + + const struct strbuf *post_payload; /* POST body to send */ + + struct curl_slist *headers; /* additional http headers to send */ + struct tempfile *tempfile; /* for response content when file */ + struct strbuf *buffer; /* for response content when strbuf */ + struct strbuf label; /* for trace2 regions */ + + struct strbuf loose_path; + + /* + * Note that I am putting all of the progress-related instance data + * inside the request-params in the hope that we can eventually + * do multi-threaded/concurrent HTTP requests when chunking + * large requests. However, the underlying "struct progress" API + * is not thread safe (that is, it doesn't allow concurrent progress + * reports (since that might require multiple lines on the screen + * or something)). + */ + enum gh__progress_state progress_state; + struct strbuf progress_base_phase2_msg; + struct strbuf progress_base_phase3_msg; + + /* + * The buffer for the formatted progress message is shared by the + * "struct progress" API and must remain valid for the duration of + * the start_progress..stop_progress lifespan. + */ + struct strbuf progress_msg; + struct progress *progress; +}; + +#define GH__REQUEST_PARAMS_INIT { \ + .b_is_post = 0, \ + .b_write_to_file = 0, \ + .b_no_cache_server = 0, \ + .object_count = 0, \ + .post_payload = NULL, \ + .headers = NULL, \ + .tempfile = NULL, \ + .buffer = NULL, \ + .label = STRBUF_INIT, \ + .loose_path = STRBUF_INIT, \ + .progress_state = GH__PROGRESS_STATE__START, \ + .progress_base_phase2_msg = STRBUF_INIT, \ + .progress_base_phase3_msg = STRBUF_INIT, \ + .progress_msg = STRBUF_INIT, \ + .progress = NULL, \ + } + +static void gh__request_params__release(struct gh__request_params *params) +{ + if (!params) + return; + + params->post_payload = NULL; /* we do not own this */ + + curl_slist_free_all(params->headers); + params->headers = NULL; + + delete_tempfile(¶ms->tempfile); + + params->buffer = NULL; /* we do not own this */ + + strbuf_release(¶ms->label); + strbuf_release(¶ms->loose_path); + + strbuf_release(¶ms->progress_base_phase2_msg); + strbuf_release(¶ms->progress_base_phase3_msg); + strbuf_release(¶ms->progress_msg); + + stop_progress(¶ms->progress); + params->progress = NULL; +} + +/* + * Bucket to describe the results of an HTTP requests (may be + * overwritten during retries so that it describes the final attempt). + */ +struct gh__response_status { + struct strbuf error_message; + struct strbuf content_type; + long response_code; /* http response code */ + CURLcode curl_code; + enum gh__error_code ec; + intmax_t bytes_received; +}; + +#define GH__RESPONSE_STATUS_INIT { \ + .error_message = STRBUF_INIT, \ + .content_type = STRBUF_INIT, \ + .response_code = 0, \ + .curl_code = CURLE_OK, \ + .ec = GH__ERROR_CODE__OK, \ + .bytes_received = 0, \ + } + +static void gh__response_status__zero(struct gh__response_status *s) +{ + strbuf_setlen(&s->error_message, 0); + strbuf_setlen(&s->content_type, 0); + s->response_code = 0; + s->curl_code = CURLE_OK; + s->ec = GH__ERROR_CODE__OK; + s->bytes_received = 0; +} + +/* + * Create a single normalized 'ec' error-code from the status we + * received from the HTTP request. Map a few of the expected HTTP + * status code to 'ec', but don't get too crazy here. + */ +static void gh__response_status__set_from_slot( + struct gh__request_params *params, + struct gh__response_status *status, + const struct active_request_slot *slot) +{ + status->curl_code = slot->results->curl_result; + gh__curlinfo_strbuf(slot->curl, CURLINFO_CONTENT_TYPE, + &status->content_type); + curl_easy_getinfo(slot->curl, CURLINFO_RESPONSE_CODE, + &status->response_code); + + strbuf_setlen(&status->error_message, 0); + + if (status->response_code == 200) + status->ec = GH__ERROR_CODE__OK; + + else if (status->response_code == 401) { + strbuf_addstr(&status->error_message, "401 Not Authorized"); + status->ec = GH__ERROR_CODE__HTTP_401; + + } else if (status->response_code == 404) { + strbuf_addstr(&status->error_message, "404 Not Found"); + status->ec = GH__ERROR_CODE__HTTP_404; + + } else if (status->curl_code != CURLE_OK) { + strbuf_addf(&status->error_message, "%s (curl)", + curl_easy_strerror(status->curl_code)); + status->ec = GH__ERROR_CODE__CURL_ERROR; + } else { + strbuf_addf(&status->error_message, "HTTP %ld Unexpected", + status->response_code); + status->ec = GH__ERROR_CODE__HTTP_UNEXPECTED_CODE; + } + + if (status->ec != GH__ERROR_CODE__OK) + status->bytes_received = 0; + else if (params->b_write_to_file) + status->bytes_received = (intmax_t)ftell(params->tempfile->fp); + else + status->bytes_received = (intmax_t)params->buffer->len; +} + +static void gh__response_status__release(struct gh__response_status *status) +{ + if (!status) + return; + strbuf_release(&status->error_message); + strbuf_release(&status->content_type); +} + +/* + * The cache-server sends a somewhat bogus 400 instead of + * the normal 401 when AUTH is required. Fixup the status + * to hide that. + */ +static void fixup_cache_server_400_to_401(struct gh__response_status *status) +{ + if (status->response_code != 400) + return; + + /* + * TODO Technically, the cache-server could send a 400 + * TODO for many reasons, not just for their bogus + * TODO pseudo-401, but we're going to assume it is a + * TODO 401 for now. We should confirm the expected + * TODO error message in the response-body. + */ + status->response_code = 401; +} + +static int gh__curl_progress_cb(void *clientp, + curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow) +{ + struct gh__request_params *params = clientp; + + /* + * From what I can tell, CURL progress arrives in 3 phases. + * + * [1] An initial connection setup phase where we get [0,0] [0,0]. + * [2] An upload phase where we start sending the request headers + * and body. ulnow will be > 0. ultotal may or may not be 0. + * [3] A download phase where we start receiving the response + * headers and payload body. dlnow will be > 0. dltotal may + * or may not be 0. + * + * If we pass zero for the total to the "struct progress" API, we + * get simple numbers rather than percentages. So our progress + * output format may vary depending. + * + * It is unclear if CURL will give us a final callback after + * everything is finished, so we leave the progress handle open + * and let the caller issue the final stop_progress(). + * + * There is a bit of a mismatch between the CURL API and the + * "struct progress" API. The latter requires us to set the + * progress message when we call one of the start_progress + * methods. We cannot change the progress message while we are + * showing progress state. And we cannot change the denominator + * (total) after we start. CURL may or may not give us the total + * sizes for each phase. + * + * Also be advised that the "struct progress" API eats messages + * so that the screen is only updated every second or so. And + * may not print anything if the start..stop happen in less then + * 2 seconds. Whereas CURL calls this callback very frequently. + * The net-net is that we may not actually see this progress + * message for small/fast HTTP requests. + */ + + switch (params->progress_state) { + case GH__PROGRESS_STATE__START: /* first callback */ + if (dlnow == 0 && ulnow == 0) + goto enter_phase_1; + + if (ulnow) + goto enter_phase_2; + else + goto enter_phase_3; + + case GH__PROGRESS_STATE__PHASE1: + if (dlnow == 0 && ulnow == 0) + return 0; + + if (ulnow) + goto enter_phase_2; + else + goto enter_phase_3; + + case GH__PROGRESS_STATE__PHASE2: + display_progress(params->progress, ulnow); + if (dlnow == 0) + return 0; + + stop_progress(¶ms->progress); + goto enter_phase_3; + + case GH__PROGRESS_STATE__PHASE3: + display_progress(params->progress, dlnow); + return 0; + + default: + return 0; + } + +enter_phase_1: + /* + * Don't bother to create a progress handle during phase [1]. + * Because we get [0,0,0,0], we don't have any data to report + * and would just have to synthesize some type of progress. + * From my testing, phase [1] is fairly quick (probably just + * the SSL handshake), so the "struct progress" API will most + * likely completely eat any messages that we did produce. + */ + params->progress_state = GH__PROGRESS_STATE__PHASE1; + return 0; + +enter_phase_2: + strbuf_setlen(¶ms->progress_msg, 0); + if (params->progress_base_phase2_msg.len) { + strbuf_addf(¶ms->progress_msg, "%s (bytes sent)", + params->progress_base_phase2_msg.buf); + params->progress = start_progress(the_repository, params->progress_msg.buf, ultotal); + display_progress(params->progress, ulnow); + } + params->progress_state = GH__PROGRESS_STATE__PHASE2; + return 0; + +enter_phase_3: + strbuf_setlen(¶ms->progress_msg, 0); + if (params->progress_base_phase3_msg.len) { + strbuf_addf(¶ms->progress_msg, "%s (bytes received)", + params->progress_base_phase3_msg.buf); + params->progress = start_progress(the_repository, params->progress_msg.buf, dltotal); + display_progress(params->progress, dlnow); + } + params->progress_state = GH__PROGRESS_STATE__PHASE3; + return 0; +} + +/* + * Run the request without using "run_one_slot()" because we + * don't want the post-request normalization, error handling, + * and auto-reauth handling in http.c. + */ +static void gh__run_one_slot(struct active_request_slot *slot, + struct gh__request_params *params, + struct gh__response_status *status) +{ + trace2_region_enter("gvfs-helper", params->label.buf, NULL); + + if (!start_active_slot(slot)) { + status->curl_code = CURLE_FAILED_INIT; /* a bit of a lie */ + strbuf_addstr(&status->error_message, + "failed to start HTTP request"); + } else { + run_active_slot(slot); + if (params->b_write_to_file) + fflush(params->tempfile->fp); + + gh__response_status__set_from_slot(params, status, slot); + + if (status->ec == GH__ERROR_CODE__OK) { + int old_len = params->label.len; + + strbuf_addstr(¶ms->label, "/nr_objects"); + trace2_data_intmax("gvfs-helper", NULL, + params->label.buf, + params->object_count); + strbuf_setlen(¶ms->label, old_len); + + strbuf_addstr(¶ms->label, "/nr_bytes"); + trace2_data_intmax("gvfs-helper", NULL, + params->label.buf, + status->bytes_received); + strbuf_setlen(¶ms->label, old_len); + } + } + + if (params->progress) + stop_progress(¶ms->progress); + + trace2_region_leave("gvfs-helper", params->label.buf, NULL); +} + +static int option_parse_cache_server_mode(const struct option *opt, + const char *arg, int unset) +{ + if (unset) /* should not happen */ + return error(_("missing value for switch '%s'"), + opt->long_name); + + else if (!strcmp(arg, "verify")) + gh__cmd_opts.cache_server_mode = + GH__CACHE_SERVER_MODE__VERIFY_DISABLE; + + else if (!strcmp(arg, "error")) + gh__cmd_opts.cache_server_mode = + GH__CACHE_SERVER_MODE__VERIFY_ERROR; + + else if (!strcmp(arg, "disable")) + gh__cmd_opts.cache_server_mode = + GH__CACHE_SERVER_MODE__DISABLE; + + else if (!strcmp(arg, "trust")) + gh__cmd_opts.cache_server_mode = + GH__CACHE_SERVER_MODE__TRUST_WITHOUT_VERIFY; + + else + return error(_("invalid value for switch '%s'"), + opt->long_name); + + return 0; +} + +/* + * Let command line args override "gvfs.sharedcache" config setting. + * + * It would be nice to move this to parse-options.c as an + * OPTION_PATHNAME handler. And maybe have flags for exists() + * and is_directory(). + */ +static int option_parse_shared_cache_directory(const struct option *opt, + const char *arg, int unset) +{ + if (unset) /* should not happen */ + return error(_("missing value for switch '%s'"), + opt->long_name); + + if (!is_directory(arg)) + return error(_("value for switch '%s' is not a directory: '%s'"), + opt->long_name, arg); + + gvfs_shared_cache_pathname = arg; + + return 0; +} + +/* + * Lookup the URL for this remote (defaults to 'origin'). + */ +static void lookup_main_url(void) +{ + /* + * Both VFS and Scalar only work with 'origin', so we expect this. + * The command line arg is mainly for debugging. + */ + if (!gh__cmd_opts.remote_name || !*gh__cmd_opts.remote_name) + gh__cmd_opts.remote_name = "origin"; + + gh__global.remote = remote_get(gh__cmd_opts.remote_name); + if (!gh__global.remote->url.v[0] || !*gh__global.remote->url.v[0]) + die("unknown remote '%s'", gh__cmd_opts.remote_name); + + /* + * Strip out any in-line auth in the origin server URL so that + * we can control which creds we fetch. + * + * Azure DevOps has been known to suggest https URLS of the + * form "https://@dev.azure.com//". + * + * Break that so that we can force the use of a PAT. + */ + gh__global.main_url = transport_anonymize_url(gh__global.remote->url.v[0]); + + trace2_data_string("gvfs-helper", NULL, "remote/url", gh__global.main_url); +} + +static void do__gvfs_config(struct gh__response_status *status, + struct strbuf *config_data); + +/* + * Find the URL of the cache-server, if we have one. + * + * This routine is called by the initialization code and is allowed + * to call die() rather than returning an 'ec'. + */ +static void select_cache_server(void) +{ + struct gh__response_status status = GH__RESPONSE_STATUS_INIT; + struct strbuf config_data = STRBUF_INIT; + const char *match = NULL; + + /* + * This only indicates that the sub-command actually called + * this routine. We rely on gh__global.cache_server_url to tell + * us if we actually have a cache-server configured. + */ + gh__global.cache_server_is_initialized = 1; + gh__global.cache_server_url = NULL; + + if (gh__cmd_opts.cache_server_mode == GH__CACHE_SERVER_MODE__DISABLE) { + trace2_data_string("gvfs-helper", NULL, "cache/url", "disabled"); + return; + } + + /* + * If the cache-server and main Git server have the same URL, we + * can silently disable the cache-server (by NOT setting the field + * in gh__global and explicitly disable the fallback logic.) + */ + if (!strcmp(gvfs_cache_server_url, gh__global.main_url)) { + gh__cmd_opts.try_fallback = 0; + trace2_data_string("gvfs-helper", NULL, "cache/url", "same"); + return; + } + + if (gh__cmd_opts.cache_server_mode == + GH__CACHE_SERVER_MODE__TRUST_WITHOUT_VERIFY) { + gh__global.cache_server_url = gvfs_cache_server_url; + trace2_data_string("gvfs-helper", NULL, "cache/url", + gvfs_cache_server_url); + return; + } + + /* + * GVFS cache-servers use the main Git server's creds rather + * than having their own creds. This feels like a security + * hole. For example, if the cache-server URL is pointed to a + * bad site, we'll happily send them our creds to the main Git + * server with each request to the cache-server. This would + * allow an attacker to later use our creds to impersonate us + * on the main Git server. + * + * So we optionally verify that the URL to the cache-server is + * well-known by the main Git server. + */ + + do__gvfs_config(&status, &config_data); + + if (status.ec == GH__ERROR_CODE__OK) { + /* + * The gvfs/config response is in JSON, but I don't think + * we need to parse it and all that. Lets just do a simple + * strstr() and assume it is sufficient. + * + * We do add some context to the pattern to guard against + * some attacks. + */ + struct strbuf pattern = STRBUF_INIT; + + strbuf_addf(&pattern, "\"Url\":\"%s\"", gvfs_cache_server_url); + match = strstr(config_data.buf, pattern.buf); + + strbuf_release(&pattern); + } + + strbuf_release(&config_data); + + if (match) { + gh__global.cache_server_url = gvfs_cache_server_url; + trace2_data_string("gvfs-helper", NULL, "cache/url", + gvfs_cache_server_url); + } + + else if (gh__cmd_opts.cache_server_mode == + GH__CACHE_SERVER_MODE__VERIFY_ERROR) { + if (status.ec != GH__ERROR_CODE__OK) + die("could not verify cache-server '%s': %s", + gvfs_cache_server_url, + status.error_message.buf); + else + die("could not verify cache-server '%s'", + gvfs_cache_server_url); + } + + else if (gh__cmd_opts.cache_server_mode == + GH__CACHE_SERVER_MODE__VERIFY_DISABLE) { + if (status.ec != GH__ERROR_CODE__OK) + warning("could not verify cache-server '%s': %s", + gvfs_cache_server_url, + status.error_message.buf); + else + warning("could not verify cache-server '%s'", + gvfs_cache_server_url); + trace2_data_string("gvfs-helper", NULL, "cache/url", + "disabled"); + } + + gh__response_status__release(&status); +} + +/* + * Read stdin until EOF (or a blank line) and add the desired OIDs + * to the oidset. + * + * Stdin should contain a list of OIDs. It may have additional + * decoration that we need to strip out. + * + * We expect: + * [] // present OIDs + */ +static unsigned long read_stdin_from_rev_list(struct oidset *oids) +{ + struct object_id oid; + struct strbuf buf_stdin = STRBUF_INIT; + unsigned long count = 0; + + do { + if (strbuf_getline(&buf_stdin, stdin) == EOF || !buf_stdin.len) + break; + + if (get_oid_hex(buf_stdin.buf, &oid)) + continue; /* just silently eat it */ + + if (!oidset_insert(oids, &oid)) + count++; + } while (1); + + strbuf_release(&buf_stdin); + return count; +} + +/* + * Build a complete JSON payload for a gvfs/objects POST request + * containing the first n OIDs in an OIDSET index by the iterator. + * + * https://github.com/microsoft/VFSForGit/blob/master/Protocol.md + */ +static unsigned long build_json_payload__gvfs_objects( + struct json_writer *jw_req, + struct oidset_iter *iter, + unsigned long nr_in_block) +{ + unsigned long k; + const struct object_id *oid; + + k = 0; + + jw_init(jw_req); + jw_object_begin(jw_req, 0); + jw_object_intmax(jw_req, "commitDepth", gh__cmd_opts.depth); + jw_object_inline_begin_array(jw_req, "objectIds"); + while (k < nr_in_block && (oid = oidset_iter_next(iter))) { + jw_array_string(jw_req, oid_to_hex(oid)); + k++; + } + jw_end(jw_req); + jw_end(jw_req); + + return k; +} + +/* + * Lookup the creds for the main/origin Git server. + */ +static void lookup_main_creds(void) +{ + if (gh__global.main_creds.username && *gh__global.main_creds.username) + return; + + credential_from_url(&gh__global.main_creds, gh__global.main_url); + credential_fill(the_repository, &gh__global.main_creds, 0); + gh__global.main_creds_need_approval = 1; +} + +/* + * If we have a set of creds for the main Git server, tell the credential + * manager to throw them away and ask it to reacquire them. + */ +static void refresh_main_creds(void) +{ + if (gh__global.main_creds.username && *gh__global.main_creds.username) + credential_reject(the_repository, &gh__global.main_creds); + + lookup_main_creds(); + + // TODO should we compare before and after values of u/p and + // TODO shortcut reauth if we already know it will fail? + // TODO if so, return a bool if same/different. +} + +static void approve_main_creds(void) +{ + if (!gh__global.main_creds_need_approval) + return; + + credential_approve(the_repository, &gh__global.main_creds); + gh__global.main_creds_need_approval = 0; +} + +/* + * Build a set of creds for the cache-server based upon the main Git + * server (assuming we have a cache-server configured). + * + * That is, we NEVER fill them directly for the cache-server -- we + * only synthesize them from the filled main creds. + */ +static void synthesize_cache_server_creds(void) +{ + if (!gh__global.cache_server_is_initialized) + BUG("sub-command did not initialize cache-server vars"); + + if (!gh__global.cache_server_url) + return; + + if (gh__global.cache_creds.username && *gh__global.cache_creds.username) + return; + + /* + * Get the main Git server creds so we can borrow the username + * and password when we talk to the cache-server. + */ + lookup_main_creds(); + free(gh__global.cache_creds.username); + gh__global.cache_creds.username = xstrdup(gh__global.main_creds.username); + free(gh__global.cache_creds.password); + gh__global.cache_creds.password = xstrdup(gh__global.main_creds.password); +} + +/* + * Flush and refresh the cache-server creds. Because the cache-server + * does not do 401s (or manage creds), we have to reload the main Git + * server creds first. + * + * That is, we NEVER reject them directly because we never filled them. + */ +static void refresh_cache_server_creds(void) +{ + credential_clear(&gh__global.cache_creds); + + refresh_main_creds(); + synthesize_cache_server_creds(); +} + +/* + * We NEVER approve cache-server creds directly because we never directly + * filled them. However, we should be able to infer that the main ones + * are valid and can approve them if necessary. + */ +static void approve_cache_server_creds(void) +{ + approve_main_creds(); +} + +/* + * Select the ODB directory where we will write objects that we + * download. If was given on the command line or define in the + * config, use the local ODB (in ".git/objects"). + */ +static void select_odb(void) +{ + const char *odb_path = NULL; + + strbuf_init(&gh__global.buf_odb_path, 0); + + if (gvfs_shared_cache_pathname && *gvfs_shared_cache_pathname) + odb_path = gvfs_shared_cache_pathname; + else { + odb_prepare_alternates(the_repository->objects); + odb_path = the_repository->objects->sources->path; + } + + strbuf_addstr(&gh__global.buf_odb_path, odb_path); +} + +/* + * Create a tempfile to stream the packfile into. + * + * We create a tempfile in the chosen ODB directory and let CURL + * automatically stream data to the file. If successful, we can + * later rename it to a proper .pack and run "git index-pack" on + * it to create the corresponding .idx file. + * + * TODO I would rather to just stream the packfile directly into + * TODO "git index-pack --stdin" (and save some I/O) because it + * TODO will automatically take care of the rename of both files + * TODO and any other cleanup. BUT INDEX-PACK WILL ONLY WRITE + * TODO TO THE PRIMARY ODB -- it will not write into the alternates + * TODO (this is considered bad form). So we would need to add + * TODO an option to index-pack to handle this. I don't want to + * TODO deal with this issue right now. + * + * TODO Consider using lockfile for this rather than naked tempfile. + */ +static struct tempfile *create_tempfile_for_packfile(void) +{ + static unsigned int nth = 0; + static struct timeval tv = {0}; + static struct tm tm = {0}; + static time_t secs = 0; + static char tbuf[32] = {0}; + + struct tempfile *tempfile = NULL; + struct strbuf buf_path = STRBUF_INIT; + + if (!nth) { + gettimeofday(&tv, NULL); + secs = tv.tv_sec; + gmtime_r(&secs, &tm); + + xsnprintf(tbuf, sizeof(tbuf), "%4d%02d%02d-%02d%02d%02d-%06ld", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + (long)tv.tv_usec); + } + + // TODO should this be in the "/pack/tempPacks/" + // TODO directory instead? YES + + strbuf_addbuf(&buf_path, &gh__global.buf_odb_path); + strbuf_complete(&buf_path, '/'); + strbuf_addf(&buf_path, "pack/vfs-%s-%04d.temp", tbuf, nth++); + + tempfile = create_tempfile(buf_path.buf); + fdopen_tempfile(tempfile, "w"); + + strbuf_release(&buf_path); + + return tempfile; +} + +/* + * Create a tempfile to stream a loose object into. + * + * We create a tempfile in the chosen ODB directory and let CURL + * automatically stream data to the file. + * + * We put it directly in the "/xx/" directory. + */ +static void create_tempfile_for_loose( + struct gh__request_params *params, + struct gh__response_status *status, + const struct object_id *oid) +{ + struct strbuf buf_path = STRBUF_INIT; + const char *hex; + + gh__response_status__zero(status); + + hex = oid_to_hex(oid); + + strbuf_addbuf(&buf_path, &gh__global.buf_odb_path); + strbuf_complete(&buf_path, '/'); + strbuf_add(&buf_path, hex, 2); + + if (!file_exists(buf_path.buf) && + mkdir(buf_path.buf, 0777) == -1 && + !file_exists(buf_path.buf)) { + strbuf_addf(&status->error_message, + "cannot create directory for loose object '%s'", + buf_path.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_CREATE_TEMPFILE; + goto cleanup; + } + + strbuf_addch(&buf_path, '/'); + strbuf_addstr(&buf_path, hex+2); + + /* Remember the full path of the final destination. */ + strbuf_setlen(¶ms->loose_path, 0); + strbuf_addbuf(¶ms->loose_path, &buf_path); + + /* + * Build a unique tempfile pathname based upon it. We avoid + * using lockfiles to avoid issues with stale locks after + * crashes. + */ + strbuf_addf(&buf_path, ".%08u.temp", getpid()); + + params->tempfile = create_tempfile(buf_path.buf); + if (!params->tempfile) { + strbuf_addstr(&status->error_message, + "could not create tempfile for loose object"); + status->ec = GH__ERROR_CODE__COULD_NOT_CREATE_TEMPFILE; + goto cleanup; + } + + fdopen_tempfile(params->tempfile, "w"); + +cleanup: + strbuf_release(&buf_path); +} + +/* + * Extract the filename portion of the given pathname. + * + * TODO Wish I could find a strbuf_filename() function for this. + */ +static void extract_filename(struct strbuf *filename, + const struct strbuf *pathname) +{ + size_t len = pathname->len; + + strbuf_setlen(filename, 0); + + while (len > 0 && !is_dir_sep(pathname->buf[len - 1])) + len--; + + strbuf_addstr(filename, &pathname->buf[len]); +} + +/* + * Convert the tempfile into a permanent .pack packfile in the ODB. + * Create the corresponding .idx file. + * + * Return the filename (not pathname) of the resulting packfile. + */ +static void install_packfile(struct gh__response_status *status, + struct tempfile **pp_tempfile, + struct strbuf *packfile_filename) +{ + struct child_process ip = CHILD_PROCESS_INIT; + struct strbuf pack_name_tmp = STRBUF_INIT; + struct strbuf pack_name_dst = STRBUF_INIT; + struct strbuf idx_name_tmp = STRBUF_INIT; + struct strbuf idx_name_dst = STRBUF_INIT; + size_t len_base; + + gh__response_status__zero(status); + + strbuf_setlen(packfile_filename, 0); + + /* + * start with ".temp" (that is owned by tempfile class). + * rename to ".pack.temp" to break ownership. + * + * create ".idx.temp" on provisional packfile. + * + * officially install both ".{pack,idx}.temp" as + * ".{pack,idx}". + */ + + strbuf_addstr(&pack_name_tmp, get_tempfile_path(*pp_tempfile)); + if (!strip_suffix(pack_name_tmp.buf, ".temp", &len_base)) { + /* + * This is more of a BUG(), but I want the error + * code propagated. + */ + strbuf_addf(&status->error_message, + "packfile tempfile does not end in '.temp': '%s'", + pack_name_tmp.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_INSTALL_PACKFILE; + goto cleanup; + } + + strbuf_setlen(&pack_name_tmp, (int)len_base); + strbuf_addbuf(&pack_name_dst, &pack_name_tmp); + strbuf_addbuf(&idx_name_tmp, &pack_name_tmp); + strbuf_addbuf(&idx_name_dst, &pack_name_tmp); + + strbuf_addstr(&pack_name_tmp, ".pack.temp"); + strbuf_addstr(&pack_name_dst, ".pack"); + strbuf_addstr(&idx_name_tmp, ".idx.temp"); + strbuf_addstr(&idx_name_dst, ".idx"); + + // TODO if either pack_name_dst or idx_name_dst already + // TODO exists in the ODB, create alternate names so that + // TODO we don't step on them. + + if (rename_tempfile(pp_tempfile, pack_name_tmp.buf) == -1) { + strbuf_addf(&status->error_message, + "could not rename packfile to '%s'", + pack_name_tmp.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_INSTALL_PACKFILE; + goto cleanup; + } + + strvec_push(&ip.args, "index-pack"); + if (gh__cmd_opts.show_progress) + strvec_push(&ip.args, "-v"); + strvec_pushl(&ip.args, "-o", idx_name_tmp.buf, NULL); + strvec_push(&ip.args, pack_name_tmp.buf); + ip.git_cmd = 1; + ip.no_stdin = 1; + ip.no_stdout = 1; + + // TODO consider capturing stdout from index-pack because + // TODO it will contain the SHA of the packfile and we can + // TODO (should?) add it to the .pack and .idx pathnames + // TODO when we install them. + // TODO + // TODO See pipe_command() rather than run_command(). + // TODO + // TODO Or should be SHA-it ourselves (or read the last 20 bytes)? + + /* + * Note that I DO NOT have a region around the index-pack process. + * The region in gh__run_one_slot() currently only covers the + * download time. This index-pack is a separate step not covered + * in the above region. Later, if/when we have CURL directly stream + * to index-pack, that region will be the combined download+index + * time. So, I'm not going to introduce it here. + */ + if (run_command(&ip)) { + unlink(pack_name_tmp.buf); + unlink(idx_name_tmp.buf); + strbuf_addf(&status->error_message, + "index-pack failed on '%s'", pack_name_tmp.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_INSTALL_PACKFILE; + goto cleanup; + } + + if (finalize_object_file(the_repository, pack_name_tmp.buf, pack_name_dst.buf) || + finalize_object_file(the_repository, idx_name_tmp.buf, idx_name_dst.buf)) { + unlink(pack_name_tmp.buf); + unlink(pack_name_dst.buf); + unlink(idx_name_tmp.buf); + unlink(idx_name_dst.buf); + strbuf_addf(&status->error_message, + "could not install packfile '%s'", + pack_name_dst.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_INSTALL_PACKFILE; + goto cleanup; + } + + extract_filename(packfile_filename, &pack_name_dst); + +cleanup: + child_process_clear(&ip); + strbuf_release(&pack_name_tmp); + strbuf_release(&pack_name_dst); + strbuf_release(&idx_name_tmp); + strbuf_release(&idx_name_dst); +} + +/* + * Convert the tempfile into a permanent loose object in the ODB. + */ +static void install_loose(struct gh__request_params *params, + struct gh__response_status *status) +{ + struct strbuf tmp_path = STRBUF_INIT; + + gh__response_status__zero(status); + + /* + * close tempfile to steal ownership away from tempfile class. + */ + strbuf_addstr(&tmp_path, get_tempfile_path(params->tempfile)); + close_tempfile_gently(params->tempfile); + + /* + * Try to install the tempfile as the actual loose object. + * + * If the loose object already exists, finalize_object_file() + * will NOT overwrite/replace it. It will silently eat the + * EEXIST error and unlink the tempfile as it if was + * successful. We just let it lie to us. + * + * Since our job is to back-fill missing objects needed by a + * foreground git process -- git should have called + * oid_object_info_extended() and loose_object_info() BEFORE + * asking us to download the missing object. So if we get a + * collision we have to assume something else is happening in + * parallel and we lost the race. And that's OK. + */ + if (finalize_object_file(the_repository, tmp_path.buf, params->loose_path.buf)) { + unlink(tmp_path.buf); + strbuf_addf(&status->error_message, + "could not install loose object '%s'", + params->loose_path.buf); + status->ec = GH__ERROR_CODE__COULD_NOT_INSTALL_LOOSE; + } + + strbuf_release(&tmp_path); +} + +/* + * Our wrapper to initialize the HTTP layer. + * + * We always use the real origin server, not the cache-server, when + * initializing the http/curl layer. + */ +static void gh_http_init(void) +{ + if (gh__global.http_is_initialized) + return; + + http_init(gh__global.remote, gh__global.main_url, 0); + gh__global.http_is_initialized = 1; +} + +static void gh_http_cleanup(void) +{ + if (!gh__global.http_is_initialized) + return; + + http_cleanup(); + gh__global.http_is_initialized = 0; +} + +static void set_main_creds_on_slot(struct active_request_slot *slot, + const struct credential *creds) +{ + assert(creds == &gh__global.main_creds); + + /* + * When talking to the main/origin server, we have 3 modes + * of operation: + * + * [1] The initial request is sent without loading creds + * and with ANY-AUTH set. (And the `":"` is a magic + * value.) + * + * This allows libcurl to negotiate for us if it can. + * For example, this allows NTLM to work by magic and + * we get 200s without ever seeing a 401. If libcurl + * cannot negotiate for us, it gives us a 401 (and all + * of the 401 code in this file responds to that). + * + * [2] A 401 retry will load the main creds and try again. + * This causes `creds->username`to be non-NULL (even + * if refers to a zero-length string). And we assume + * BASIC Authentication. (And a zero-length username + * is a convention for PATs, but then sometimes users + * put the PAT in their `username` field and leave the + * `password` field blank. And that works too.) + * + * [3] Subsequent requests on the same connection use + * whatever worked before. + */ + if (creds && creds->username) { + curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(slot->curl, CURLOPT_USERNAME, creds->username); + curl_easy_setopt(slot->curl, CURLOPT_PASSWORD, creds->password); + } else { + curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_easy_setopt(slot->curl, CURLOPT_USERPWD, ":"); + } +} + +static void set_cache_server_creds_on_slot(struct active_request_slot *slot, + const struct credential *creds) +{ + assert(creds == &gh__global.cache_creds); + assert(creds->username); + + /* + * Things are weird when talking to a cache-server: + * + * [1] They don't send 401s on an auth error, rather they send + * a 400 (with a nice human-readable string in the html body). + * This prevents libcurl from doing any negotiation for us. + * + * [2] Cache-servers don't manage their own passwords, but + * rather require us to send the Basic Authentication + * username & password that we would send to the main + * server. (So yes, we have to get creds validated + * against the main server creds and substitute them when + * talking to the cache-server.) + * + * This means that: + * + * [a] We cannot support cache-servers that want to use NTLM. + * + * [b] If we want to talk to a cache-server, we have get the + * Basic Auth creds for the main server. And this may be + * problematic if the libcurl and/or the credential manager + * insists on using NTLM and prevents us from getting them. + * + * So we never try AUTH-ANY and force Basic Auth (if possible). + */ + if (creds && creds->username) { + curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(slot->curl, CURLOPT_USERNAME, creds->username); + curl_easy_setopt(slot->curl, CURLOPT_PASSWORD, creds->password); + } +} + +/* + * Do a single HTTP request without auth-retry or fallback. + */ +static void do_req(const char *url_base, + const char *url_component, + const struct credential *creds, + struct gh__request_params *params, + struct gh__response_status *status) +{ + struct active_request_slot *slot; + struct slot_results results; + struct strbuf rest_url = STRBUF_INIT; + + gh__response_status__zero(status); + + if (params->b_write_to_file) { + // TODO ftruncate tempfile ?? + } else { + strbuf_setlen(params->buffer, 0); + } + + end_url_with_slash(&rest_url, url_base); + strbuf_addstr(&rest_url, url_component); + + slot = get_active_slot(); + slot->results = &results; + + curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 0L); /* not a HEAD request */ + curl_easy_setopt(slot->curl, CURLOPT_URL, rest_url.buf); + curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, params->headers); + if (curl_version_info(CURLVERSION_NOW)->version_num < 0x074b00) + /* + * cURL 7.75.0 allows headers to be parsed even when + * `CURLOPT_FAILONERROR` is enabled and the HTTP result code + * indicates an error. This is the behavior expected by + * `gvfs-helper`. + * + * On older cURL versions, `gvfs-helper` still needs to parse + * the HTTP headers and therefore needs to _not_ fail upon + * HTTP result codes indicating errors; For newer cURL + * versions, we still prefer to enable `FAILONERROR`. + */ + curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, (long)0); + + if (params->b_is_post) { + curl_easy_setopt(slot->curl, CURLOPT_POST, 1L); + curl_easy_setopt(slot->curl, CURLOPT_ENCODING, NULL); + curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDS, + params->post_payload->buf); + curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDSIZE, + (long)params->post_payload->len); + } else { + curl_easy_setopt(slot->curl, CURLOPT_POST, 0L); + } + + if (params->b_write_to_file) { + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, fwrite); + curl_easy_setopt(slot->curl, CURLOPT_WRITEDATA, + (void*)params->tempfile->fp); + } else { + curl_easy_setopt(slot->curl, CURLOPT_WRITEFUNCTION, + fwrite_buffer); + curl_easy_setopt(slot->curl, CURLOPT_FILE, params->buffer); + } + + if (url_base == gh__global.main_url) + set_main_creds_on_slot(slot, creds); + else + set_cache_server_creds_on_slot(slot, creds); + + if (params->progress_base_phase2_msg.len || + params->progress_base_phase3_msg.len) { + curl_easy_setopt(slot->curl, CURLOPT_XFERINFOFUNCTION, + gh__curl_progress_cb); + curl_easy_setopt(slot->curl, CURLOPT_XFERINFODATA, params); + curl_easy_setopt(slot->curl, CURLOPT_NOPROGRESS, 0L); + } else { + curl_easy_setopt(slot->curl, CURLOPT_NOPROGRESS, 1L); + } + + gh__run_one_slot(slot, params, status); + strbuf_release(&rest_url); +} + +static void do_req__to_main(const char *url_component, + struct gh__request_params *params, + struct gh__response_status *status) +{ +// lookup_main_creds(); + + /* + * When talking to the main Git server, we DO NOT preload the + * creds before the first request. + */ + + do_req(gh__global.main_url, url_component, &gh__global.main_creds, + params, status); + + if (status->response_code == 401) { + refresh_main_creds(); + + do_req(gh__global.main_url, url_component, &gh__global.main_creds, + params, status); + } + + if (status->response_code == 200) + approve_main_creds(); +} + +static void do_req__to_cache_server(const char *url_component, + struct gh__request_params *params, + struct gh__response_status *status) +{ + /* + * When talking to a cache-server, DO force load the creds. + * This implicitly preloads the creds to the main server. + */ + synthesize_cache_server_creds(); + + do_req(gh__global.cache_server_url, url_component, &gh__global.cache_creds, + params, status); + fixup_cache_server_400_to_401(status); + + if (status->response_code == 401) { + refresh_cache_server_creds(); + + do_req(gh__global.cache_server_url, url_component, + &gh__global.cache_creds, params, status); + fixup_cache_server_400_to_401(status); + } + + if (status->response_code == 200) + approve_cache_server_creds(); +} + +static void do_req__with_fallback(const char *url_component, + struct gh__request_params *params, + struct gh__response_status *status) +{ + if (gh__global.cache_server_url && !params->b_no_cache_server) { + do_req__to_cache_server(url_component, params, status); + + if (status->response_code == 200) + return; + + if (!gh__cmd_opts.try_fallback) + return; + + /* + * The cache-server shares creds with the main Git server, + * so if our creds failed against the cache-server, they + * will also fail against the main Git server. We just let + * this fail. + * + * Falling-back would likely just cause the 3rd (or maybe + * 4th) cred prompt. + */ + if (status->response_code == 401) + return; + } + + do_req__to_main(url_component, params, status); +} + +/* + * Call "gvfs/config" REST API. + * + * Return server's response buffer. This is probably a raw JSON string. + */ +static void do__gvfs_config(struct gh__response_status *status, + struct strbuf *config_data) +{ + struct gh__request_params params = GH__REQUEST_PARAMS_INIT; + + strbuf_addstr(¶ms.label, "GET/config"); + + params.b_is_post = 0; + params.b_write_to_file = 0; + params.b_no_cache_server = 1; /* they don't handle gvfs/config API */ + params.buffer = config_data; + + params.object_count = 1; /* a bit of a lie */ + + /* + * "X-TFS-FedAuthRedirect: Suppress" disables the 302 + 203 redirect + * sequence to a login page and forces the main Git server to send a + * normal 401. + */ + params.headers = http_copy_default_headers(); + params.headers = curl_slist_append(params.headers, + "X-TFS-FedAuthRedirect: Suppress"); + params.headers = curl_slist_append(params.headers, + "Pragma: no-cache"); + + if (gh__cmd_opts.show_progress) { + /* + * gvfs/config has a very small reqest payload, so I don't + * see any need to report progress on the upload side of + * the GET. So just report progress on the download side. + */ + strbuf_addstr(¶ms.progress_base_phase3_msg, + "Receiving gvfs/config"); + } + + do_req__with_fallback("gvfs/config", ¶ms, status); + + gh__request_params__release(¶ms); +} + +/* + * Call "gvfs/objects/" REST API to fetch a loose object + * and write it to the ODB. + */ +static void do__loose__gvfs_object(struct gh__response_status *status, + const struct object_id *oid) +{ + struct gh__request_params params = GH__REQUEST_PARAMS_INIT; + struct strbuf component_url = STRBUF_INIT; + + gh__response_status__zero(status); + + strbuf_addf(&component_url, "gvfs/objects/%s", oid_to_hex(oid)); + + strbuf_addstr(¶ms.label, "GET/objects"); + + params.b_is_post = 0; + params.b_write_to_file = 1; + params.b_no_cache_server = 0; + + params.object_count = 1; + + params.headers = http_copy_default_headers(); + params.headers = curl_slist_append(params.headers, + "X-TFS-FedAuthRedirect: Suppress"); + params.headers = curl_slist_append(params.headers, + "Pragma: no-cache"); + + create_tempfile_for_loose(¶ms, status, oid); + if (!params.tempfile) + goto cleanup; + + if (gh__cmd_opts.show_progress) { + /* + * Likewise, a gvfs/objects/{oid} has a very small reqest + * payload, so I don't see any need to report progress on + * the upload side of the GET. So just report progress + * on the download side. + */ + strbuf_addstr(¶ms.progress_base_phase3_msg, + "Receiving 1 loose object"); + } + + do_req__with_fallback(component_url.buf, ¶ms, status); + + if (status->ec == GH__ERROR_CODE__OK) + install_loose(¶ms, status); + +cleanup: + gh__request_params__release(¶ms); + strbuf_release(&component_url); +} + +/* + * Call "gvfs/objects" POST REST API to fetch a packfile containing + * the objects in the requested OIDSET. Returns the filename (not + * pathname) to the new packfile. + */ +static void do__packfile__gvfs_objects(struct gh__response_status *status, + struct oidset_iter *iter, + unsigned long nr_wanted_in_block, + struct strbuf *output_filename, + unsigned long *nr_taken) +{ + struct json_writer jw_req = JSON_WRITER_INIT; + struct gh__request_params params = GH__REQUEST_PARAMS_INIT; + + gh__response_status__zero(status); + + params.object_count = build_json_payload__gvfs_objects( + &jw_req, iter, nr_wanted_in_block); + *nr_taken = params.object_count; + + strbuf_addstr(¶ms.label, "POST/objects"); + + params.b_is_post = 1; + params.b_write_to_file = 1; + params.b_no_cache_server = 0; + + params.post_payload = &jw_req.json; + + params.headers = http_copy_default_headers(); + params.headers = curl_slist_append(params.headers, + "X-TFS-FedAuthRedirect: Suppress"); + params.headers = curl_slist_append(params.headers, + "Pragma: no-cache"); + params.headers = curl_slist_append(params.headers, + "Content-Type: application/json"); + /* + * We really always want a packfile. But if the payload only + * requests 1 OID, the server will/may send us a single loose + * objects instead. (Apparently the server ignores us when we + * only send application/x-git-packfile and does it anyway.) + * + * So to make it clear to my future self, go ahead and add + * an accept header for loose objects and own it. + */ + params.headers = curl_slist_append(params.headers, + "Accept: application/x-git-packfile"); + params.headers = curl_slist_append(params.headers, + "Accept: application/x-git-loose-object"); + + params.tempfile = create_tempfile_for_packfile(); + if (!params.tempfile) { + strbuf_addstr(&status->error_message, + "could not create tempfile for packfile"); + status->ec = GH__ERROR_CODE__COULD_NOT_CREATE_TEMPFILE; + goto cleanup; + } + + if (gh__cmd_opts.show_progress) { + strbuf_addf(¶ms.progress_base_phase2_msg, + "Requesting packfile with %ld objects", + params.object_count); + strbuf_addf(¶ms.progress_base_phase3_msg, + "Receiving packfile with %ld objects", + params.object_count); + } + + do_req__with_fallback("gvfs/objects", ¶ms, status); + + if (status->ec == GH__ERROR_CODE__OK) { + if (!strcmp(status->content_type.buf, + "application/x-git-packfile")) { + + // TODO Consider having a worker thread to manage + // TODO running index-pack and then install the + // TODO resulting .idx and .pack files. This would + // TODO let us interleave those steps with our thread + // TODO fetching the next block of objects from the + // TODO server. (Need to think about how progress + // TODO messages from our thread and index-pack + // TODO would mesh.) + // TODO + // TODO But then again, if we hack index-pack to write + // TODO to our alternate and stream the data thru it, + // TODO it won't matter. + + install_packfile(status, ¶ms.tempfile, + output_filename); + goto cleanup; + } + + if (!strcmp(status->content_type.buf, + "application/x-git-loose-object")) + { + /* + * This should not happen (when we request + * more than one object). The server can send + * us a loose object (even when we use the + * POST form) if there is only one object in + * the payload (and despite the set of accept + * headers we send), so I'm going to leave + * this here. + */ + strbuf_addstr(&status->error_message, + "received loose object when packfile expected"); + status->ec = GH__ERROR_CODE__UNEXPECTED_CONTENT_TYPE; + goto cleanup; + } + + strbuf_addf(&status->error_message, + "received unknown content-type '%s'", + status->content_type.buf); + status->ec = GH__ERROR_CODE__UNEXPECTED_CONTENT_TYPE; + goto cleanup; + } + +cleanup: + gh__request_params__release(¶ms); + jw_release(&jw_req); +} + +/* + * Bulk or individually fetch a list of objects in one or more http requests. + * Create one or more packfiles and/or loose objects. + * + * We accumulate results for each request in `result_list` until we get a + * hard error and have to stop. + */ +static void do_fetch_oidset(struct gh__response_status *status, + struct oidset *oids, + unsigned long nr_total, + struct string_list *result_list) +{ + struct oidset_iter iter; + struct strbuf output_filename = STRBUF_INIT; + struct strbuf msg = STRBUF_INIT; + struct strbuf err404 = STRBUF_INIT; + const struct object_id *oid; + unsigned long k; + unsigned long nr_taken; + int had_404 = 0; + + gh__response_status__zero(status); + if (!nr_total) + return; + + oidset_iter_init(oids, &iter); + + for (k = 0; k < nr_total; k += nr_taken) { + if (nr_total - k == 1 || gh__cmd_opts.block_size == 1) { + oid = oidset_iter_next(&iter); + nr_taken = 1; + + do__loose__gvfs_object(status, oid); + + /* + * If we get a 404 for an individual object, ignore + * it and get the rest. We'll fixup the 'ec' later. + */ + if (status->ec == GH__ERROR_CODE__HTTP_404) { + if (!err404.len) + strbuf_addf(&err404, "%s: loose object %s", + status->error_message.buf, + oid_to_hex(oid)); + /* + * Mark the fetch as "incomplete", but don't + * stop trying to get other chunks. + */ + had_404 = 1; + continue; + } + + if (status->ec != GH__ERROR_CODE__OK) { + /* Stop at the first hard error. */ + strbuf_addf(&status->error_message, ": loose %s", + oid_to_hex(oid)); + goto cleanup; + } + + strbuf_setlen(&msg, 0); + strbuf_addf(&msg, "loose %s", oid_to_hex(oid)); + string_list_append(result_list, msg.buf); + + } else { + strbuf_setlen(&output_filename, 0); + + do__packfile__gvfs_objects(status, &iter, + gh__cmd_opts.block_size, + &output_filename, + &nr_taken); + + /* + * Because the oidset iterator has random + * order, it does no good to say the k-th or + * n-th chunk was incomplete; the client + * cannot use that index for anything. + * + * We get a 404 when at least one object in + * the chunk was not found. + * + * TODO Consider various retry strategies (such as + * TODO loose or bisect) on the members within this + * TODO chunk to reduce the impact of the miss. + * + * For now, ignore the 404 and go on to the + * next chunk and then fixup the 'ec' later. + */ + if (status->ec == GH__ERROR_CODE__HTTP_404) { + if (!err404.len) + strbuf_addf(&err404, + "%s: packfile object", + status->error_message.buf); + /* + * Mark the fetch as "incomplete", but don't + * stop trying to get other chunks. + */ + had_404 = 1; + continue; + } + + if (status->ec != GH__ERROR_CODE__OK) { + /* Stop at the first hard error. */ + strbuf_addstr(&status->error_message, + ": in packfile"); + goto cleanup; + } + + strbuf_setlen(&msg, 0); + strbuf_addf(&msg, "packfile %s", output_filename.buf); + string_list_append(result_list, msg.buf); + } + } + +cleanup: + strbuf_release(&msg); + strbuf_release(&err404); + strbuf_release(&output_filename); + + if (had_404 && status->ec == GH__ERROR_CODE__OK) { + strbuf_setlen(&status->error_message, 0); + strbuf_addstr(&status->error_message, "404 Not Found"); + status->ec = GH__ERROR_CODE__HTTP_404; + } +} + +/* + * Finish with initialization. This happens after the main option + * parsing, dispatch to sub-command, and sub-command option parsing + * and before actually doing anything. + * + * Optionally configure the cache-server if the sub-command will + * use it. + */ +static void finish_init(int setup_cache_server) +{ + select_odb(); + + lookup_main_url(); + gh_http_init(); + + if (setup_cache_server) + select_cache_server(); +} + +/* + * Request gvfs/config from main Git server. (Config data is not + * available from a GVFS cache-server.) + * + * Print the received server configuration (as the raw JSON string). + */ +static enum gh__error_code do_sub_cmd__config(int argc UNUSED, const char **argv UNUSED) +{ + struct gh__response_status status = GH__RESPONSE_STATUS_INIT; + struct strbuf config_data = STRBUF_INIT; + enum gh__error_code ec = GH__ERROR_CODE__OK; + + trace2_cmd_mode("config"); + + finish_init(0); + + do__gvfs_config(&status, &config_data); + ec = status.ec; + + if (ec == GH__ERROR_CODE__OK) + printf("%s\n", config_data.buf); + else + error("config: %s", status.error_message.buf); + + gh__response_status__release(&status); + strbuf_release(&config_data); + + return ec; +} + +/* + * Read a list of objects from stdin and fetch them in a single request (or + * multiple block-size requests). + */ +static enum gh__error_code do_sub_cmd__get(int argc, const char **argv) +{ + static struct option get_options[] = { + OPT_UNSIGNED('b', "block-size", &gh__cmd_opts.block_size, + N_("number of objects to request at a time")), + OPT_INTEGER('d', "depth", &gh__cmd_opts.depth, + N_("Commit depth")), + OPT_END(), + }; + + struct gh__response_status status = GH__RESPONSE_STATUS_INIT; + struct oidset oids = OIDSET_INIT; + struct string_list result_list = STRING_LIST_INIT_DUP; + enum gh__error_code ec = GH__ERROR_CODE__OK; + unsigned long nr_total; + size_t k; + + trace2_cmd_mode("get"); + + if (argc > 1 && !strcmp(argv[1], "-h")) + usage_with_options(get_usage, get_options); + + argc = parse_options(argc, argv, NULL, get_options, get_usage, 0); + if (gh__cmd_opts.depth < 1) + gh__cmd_opts.depth = 1; + + finish_init(1); + + nr_total = read_stdin_from_rev_list(&oids); + + trace2_region_enter("gvfs-helper", "get", NULL); + trace2_data_intmax("gvfs-helper", NULL, "get/nr_objects", nr_total); + do_fetch_oidset(&status, &oids, nr_total, &result_list); + trace2_region_leave("gvfs-helper", "get", NULL); + + ec = status.ec; + + for (k = 0; k < result_list.nr; k++) + printf("%s\n", result_list.items[k].string); + + if (ec != GH__ERROR_CODE__OK) + error("get: %s", status.error_message.buf); + + gh__response_status__release(&status); + oidset_clear(&oids); + string_list_clear(&result_list, 0); + + return ec; +} + +/* + * Handle the 'get' command when in "server mode". Only call error() + * for hard errors where we cannot communicate correctly with the foreground + * client process. Pass any actual data errors (such as 404's or 401's from + * the fetch back to the client process. + */ +static enum gh__error_code do_server_subprocess_get(void) +{ + struct gh__response_status status = GH__RESPONSE_STATUS_INIT; + struct oidset oids = OIDSET_INIT; + struct object_id oid; + struct string_list result_list = STRING_LIST_INIT_DUP; + enum gh__error_code ec = GH__ERROR_CODE__OK; + char *line; + int len; + int err; + size_t k; + unsigned long nr_total = 0; + + /* + * Inside the "get" command, we expect a list of OIDs + * and a flush. + */ + while (1) { + len = packet_read_line_gently(0, NULL, &line); + if (len < 0 || !line) + break; + + if (get_oid_hex(line, &oid)) { + error("server: invalid oid syntax '%s'", line); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + + if (!oidset_insert(&oids, &oid)) + nr_total++; + } + + if (!nr_total) { + if (packet_write_fmt_gently(1, "ok\n")) { + error("server: cannot write 'get' result to client"); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + } else + ec = GH__ERROR_CODE__OK; + goto cleanup; + } + + trace2_region_enter("gvfs-helper", "server/get", NULL); + trace2_data_intmax("gvfs-helper", NULL, "server/get/nr_objects", nr_total); + do_fetch_oidset(&status, &oids, nr_total, &result_list); + trace2_region_leave("gvfs-helper", "server/get", NULL); + + /* + * Write pathname of the ODB where we wrote all of the objects + * we fetched. + */ + if (packet_write_fmt_gently(1, "odb %s\n", + gh__global.buf_odb_path.buf)) { + error("server: cannot write 'odb' to client"); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + + for (k = 0; k < result_list.nr; k++) + if (packet_write_fmt_gently(1, "%s\n", + result_list.items[k].string)) + { + error("server: cannot write result to client: '%s'", + result_list.items[k].string); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + + err = 0; + if (ec == GH__ERROR_CODE__OK) + err = packet_write_fmt_gently(1, "ok\n"); + else if (ec == GH__ERROR_CODE__HTTP_404) + err = packet_write_fmt_gently(1, "partial\n"); + else + err = packet_write_fmt_gently(1, "error %s\n", + status.error_message.buf); + if (err) { + error("server: cannot write result to client"); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + + if (packet_flush_gently(1)) { + error("server: cannot flush result to client"); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + +cleanup: + oidset_clear(&oids); + string_list_clear(&result_list, 0); + gh__response_status__release(&status); + + return ec; +} + +typedef enum gh__error_code (fn_subprocess_cmd)(void); + +struct subprocess_capability { + const char *name; + int client_has; + fn_subprocess_cmd *pfn; +}; + +static struct subprocess_capability caps[] = { + { "get", 0, do_server_subprocess_get }, + { NULL, 0, NULL }, +}; + +/* + * Handle the subprocess protocol handshake as described in: + * [] Documentation/technical/protocol-common.txt + * [] Documentation/technical/long-running-process-protocol.txt + */ +static int do_protocol_handshake(void) +{ +#define OUR_SUBPROCESS_VERSION "1" + + char *line; + int len; + int k; + int b_support_our_version = 0; + + len = packet_read_line_gently(0, NULL, &line); + if (len < 0 || !line || strcmp(line, "gvfs-helper-client")) { + error("server: subprocess welcome handshake failed: %s", line); + return -1; + } + + while (1) { + const char *v; + len = packet_read_line_gently(0, NULL, &line); + if (len < 0 || !line) + break; + if (!skip_prefix(line, "version=", &v)) { + error("server: subprocess version handshake failed: %s", + line); + return -1; + } + b_support_our_version |= (!strcmp(v, OUR_SUBPROCESS_VERSION)); + } + if (!b_support_our_version) { + error("server: client does not support our version: %s", + OUR_SUBPROCESS_VERSION); + return -1; + } + + if (packet_write_fmt_gently(1, "gvfs-helper-server\n") || + packet_write_fmt_gently(1, "version=%s\n", + OUR_SUBPROCESS_VERSION) || + packet_flush_gently(1)) { + error("server: cannot write version handshake"); + return -1; + } + + while (1) { + const char *v; + int k; + + len = packet_read_line_gently(0, NULL, &line); + if (len < 0 || !line) + break; + if (!skip_prefix(line, "capability=", &v)) { + error("server: subprocess capability handshake failed: %s", + line); + return -1; + } + for (k = 0; caps[k].name; k++) + if (!strcmp(v, caps[k].name)) + caps[k].client_has = 1; + } + + for (k = 0; caps[k].name; k++) + if (caps[k].client_has) + if (packet_write_fmt_gently(1, "capability=%s\n", + caps[k].name)) { + error("server: cannot write capabilities handshake: %s", + caps[k].name); + return -1; + } + if (packet_flush_gently(1)) { + error("server: cannot write capabilities handshake"); + return -1; + } + + return 0; +} + +/* + * Interactively listen to stdin for a series of commands and execute them. + */ +static enum gh__error_code do_sub_cmd__server(int argc, const char **argv) +{ + static struct option server_options[] = { + OPT_UNSIGNED('b', "block-size", &gh__cmd_opts.block_size, + N_("number of objects to request at a time")), + OPT_INTEGER('d', "depth", &gh__cmd_opts.depth, + N_("Commit depth")), + OPT_END(), + }; + + enum gh__error_code ec = GH__ERROR_CODE__OK; + char *line; + int len; + int k; + + trace2_cmd_mode("server"); + + if (argc > 1 && !strcmp(argv[1], "-h")) + usage_with_options(server_usage, server_options); + + argc = parse_options(argc, argv, NULL, server_options, server_usage, 0); + if (gh__cmd_opts.depth < 1) + gh__cmd_opts.depth = 1; + + finish_init(1); + + if (do_protocol_handshake()) { + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + +top_of_loop: + while (1) { + len = packet_read_line_gently(0, NULL, &line); + if (len < 0 || !line) { + /* use extra FLUSH as a QUIT */ + ec = GH__ERROR_CODE__OK; + goto cleanup; + } + + for (k = 0; caps[k].name; k++) { + if (caps[k].client_has && !strcmp(line, caps[k].name)) { + ec = (caps[k].pfn)(); + if (ec != GH__ERROR_CODE__OK) + goto cleanup; + goto top_of_loop; + } + } + + error("server: unknown command '%s'", line); + ec = GH__ERROR_CODE__SUBPROCESS_SYNTAX; + goto cleanup; + } + +cleanup: + return ec; +} + +static enum gh__error_code do_sub_cmd(int argc, const char **argv) +{ + if (!strcmp(argv[0], "get")) + return do_sub_cmd__get(argc, argv); + + if (!strcmp(argv[0], "config")) + return do_sub_cmd__config(argc, argv); + + if (!strcmp(argv[0], "server")) + return do_sub_cmd__server(argc, argv); + + // TODO have "test" mode that could be used to drive + // TODO unit testing. + + return GH__ERROR_CODE__USAGE; +} + +/* + * Communicate with the primary Git server or a GVFS cache-server using the + * GVFS Protocol. + * + * https://github.com/microsoft/VFSForGit/blob/master/Protocol.md + */ +int cmd_main(int argc, const char **argv) +{ + static struct option main_options[] = { + OPT_STRING('r', "remote", &gh__cmd_opts.remote_name, + N_("remote"), + N_("Remote name")), + OPT_BOOL('f', "fallback", &gh__cmd_opts.try_fallback, + N_("Fallback to Git server if cache-server fails")), + OPT_CALLBACK(0, "cache-server", NULL, + N_("cache-server"), + N_("cache-server=disable|trust|verify|error"), + option_parse_cache_server_mode), + OPT_CALLBACK(0, "shared-cache", NULL, + N_("pathname"), + N_("Pathname to shared objects directory"), + option_parse_shared_cache_directory), + OPT_BOOL('p', "progress", &gh__cmd_opts.show_progress, + N_("Show progress")), + OPT_END(), + }; + + enum gh__error_code ec = GH__ERROR_CODE__OK; + + if (argc > 1 && !strcmp(argv[1], "-h")) + usage_with_options(main_usage, main_options); + + trace2_cmd_name("gvfs-helper"); + + setup_git_directory_gently(NULL); + + repo_config(the_repository, git_default_config, NULL); + + /* Set any non-zero initial values in gh__cmd_opts. */ + gh__cmd_opts.depth = 1; + gh__cmd_opts.block_size = GH__DEFAULT_BLOCK_SIZE; + gh__cmd_opts.show_progress = !!isatty(2); + + argc = parse_options(argc, argv, NULL, main_options, main_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + if (argc == 0) + usage_with_options(main_usage, main_options); + + ec = do_sub_cmd(argc, argv); + + gh_http_cleanup(); + + if (ec == GH__ERROR_CODE__USAGE) + usage_with_options(main_usage, main_options); + + return ec; +} diff --git a/meson.build b/meson.build index 62363511c4eaad..2a334d7a674b23 100644 --- a/meson.build +++ b/meson.build @@ -353,6 +353,7 @@ libgit_sources = [ 'graph.c', 'grep.c', 'gvfs.c', + 'gvfs-helper-client.c', 'hash-lookup.c', 'hash.c', 'hashmap.c', @@ -1869,6 +1870,13 @@ if curl.found() ) endif + test_dependencies += executable('git-gvfs-helper', + sources: 'gvfs-helper.c', + dependencies: [libgit_curl], + install: true, + install_dir: get_option('libexecdir') / 'git-core', + ) + foreach alias : [ 'git-remote-https', 'git-remote-ftp', 'git-remote-ftps' ] test_dependencies += executable(alias, sources: 'remote-curl.c', @@ -1903,6 +1911,7 @@ endforeach foreach symlink : [ 'git', + 'git-gvfs-helper', 'git-receive-pack', 'git-shell', 'git-upload-archive', diff --git a/odb.c b/odb.c index d20e62e8393fa7..5cb0e619a721d0 100644 --- a/odb.c +++ b/odb.c @@ -6,6 +6,7 @@ #include "environment.h" #include "gettext.h" #include "gvfs.h" +#include "gvfs-helper-client.h" #include "hex.h" #include "hook.h" #include "khash.h" @@ -787,7 +788,7 @@ static int do_oid_object_info_extended(struct object_database *odb, const struct object_id *real = oid; int already_retried = 0; int tried_hook = 0; - + int tried_gvfs_helper = 0; if (flags & OBJECT_INFO_LOOKUP_REPLACE) real = lookup_replace_object(odb->repo, oid); @@ -816,6 +817,8 @@ static int do_oid_object_info_extended(struct object_database *odb, } while (1) { + extern int core_use_gvfs_helper; + if (find_pack_entry(odb->repo, real, &e)) break; @@ -823,13 +826,41 @@ static int do_oid_object_info_extended(struct object_database *odb, if (!loose_object_info(odb->repo, real, oi, flags)) return 0; + if (core_use_gvfs_helper && !tried_gvfs_helper) { + enum gh_client__created ghc; + + if (flags & OBJECT_INFO_SKIP_FETCH_OBJECT) + return -1; + + gh_client__get_immediate(real, &ghc); + tried_gvfs_helper = 1; + + /* + * Retry the lookup IIF `gvfs-helper` created one + * or more new packfiles or loose objects. + */ + if (ghc != GHC__CREATED__NOTHING) + continue; + + /* + * If `gvfs-helper` fails, we just want to return -1. + * But allow the other providers to have a shot at it. + * (At least until we have a chance to consolidate + * them.) + */ + } + /* Not a loose object; someone else may have just packed it. */ if (!(flags & OBJECT_INFO_QUICK)) { odb_reprepare(odb->repo->objects); if (find_pack_entry(odb->repo, real, &e)) break; if (gvfs_virtualize_objects(odb->repo) && !tried_hook) { + // TODO Assert or at least trace2 if gvfs-helper + // TODO was tried and failed and then read-object-hook + // TODO is successful at getting this object. tried_hook = 1; + // TODO BUG? Should 'oid' be 'real' ? if (!read_object_process(odb->repo, oid)) goto retry; } diff --git a/promisor-remote.c b/promisor-remote.c index 77ebf537e2b3ee..8800476e2e8036 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -1,7 +1,9 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "environment.h" #include "gettext.h" +#include "gvfs-helper-client.h" #include "hex.h" #include "odb.h" #include "promisor-remote.h" @@ -220,7 +222,7 @@ struct promisor_remote *repo_promisor_remote_find(struct repository *r, int repo_has_promisor_remote(struct repository *r) { - return !!repo_promisor_remote_find(r, NULL); + return core_use_gvfs_helper || !!repo_promisor_remote_find(r, NULL); } int repo_has_accepted_promisor_remote(struct repository *r) @@ -279,6 +281,15 @@ void promisor_remote_get_direct(struct repository *repo, if (oid_nr == 0) return; + if (core_use_gvfs_helper) { + enum gh_client__created ghc = GHC__CREATED__NOTHING; + + trace2_data_intmax("bug", the_repository, "fetch_objects/gvfs-helper", oid_nr); + gh_client__queue_oid_array(oids, oid_nr); + if (!gh_client__drain_queue(&ghc)) + return; + die(_("failed to fetch missing objects from the remote")); + } promisor_remote_init(repo); diff --git a/t/helper/.gitignore b/t/helper/.gitignore index 8c2ddcce95f7aa..4687ed470c5978 100644 --- a/t/helper/.gitignore +++ b/t/helper/.gitignore @@ -1,2 +1,3 @@ +/test-gvfs-protocol /test-tool /test-fake-ssh diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 37371e3f5e3e4c..a3bf74d1af2800 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -115,7 +115,7 @@ do credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \ daemon | \ difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \ - gui | gui--askpass | \ + gui | gui--askpass | gvfs-helper | \ http-backend | http-fetch | http-push | init-db | \ merge-octopus | merge-one-file | merge-resolve | mergetool | \ mktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \ From 130827eeb08f63c9b4d771a4de2c5fcea450fd1c Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Thu, 25 Jul 2019 15:43:50 -0400 Subject: [PATCH 123/229] trace2:gvfs:experiment: increase default event depth for unpack-tree data --- trace2/tr2_tgt_event.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trace2/tr2_tgt_event.c b/trace2/tr2_tgt_event.c index 5a0381791f7eb4..5ecc0b920e3694 100644 --- a/trace2/tr2_tgt_event.c +++ b/trace2/tr2_tgt_event.c @@ -39,7 +39,7 @@ static struct tr2_dst tr2dst_event = { * event target. Use the TR2_SYSENV_EVENT_NESTING setting to increase * region details in the event target. */ -static int tr2env_event_max_nesting_levels = 2; +static int tr2env_event_max_nesting_levels = 4; /* * Use the TR2_SYSENV_EVENT_BRIEF to omit the