From ee700384376e25c6da1dedd95562a5352cfa2020 Mon Sep 17 00:00:00 2001 From: Malcolm Wood Date: Thu, 19 Jun 2025 14:02:52 +0100 Subject: [PATCH 1/5] Support for the "shallow-since" option in fetch requests. --- include/git2/remote.h | 23 ++++- include/git2/sys/transport.h | 1 + src/libgit2/fetch.c | 1 + src/libgit2/transports/local.c | 6 ++ src/libgit2/transports/smart_pkt.c | 27 +++++ src/libgit2/transports/smart_protocol.c | 2 +- tests/libgit2/online/shallow.c | 130 ++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 4 deletions(-) diff --git a/include/git2/remote.h b/include/git2/remote.h index 2472dedccf8..2b7a3a4243b 100644 --- a/include/git2/remote.h +++ b/include/git2/remote.h @@ -762,7 +762,10 @@ typedef enum { GIT_FETCH_DEPTH_FULL = 0, /** The fetch should "unshallow" and fetch missing data. */ - GIT_FETCH_DEPTH_UNSHALLOW = 2147483647 + GIT_FETCH_DEPTH_UNSHALLOW = 2147483647, + + /** No "shallow-since" date specified. */ + GIT_FETCH_SINCE_UNSPECIFIED = -1 } git_fetch_depth_t; /** @@ -808,12 +811,22 @@ typedef struct { /** * Depth of the fetch to perform, or `GIT_FETCH_DEPTH_FULL` * (or `0`) for full history, or `GIT_FETCH_DEPTH_UNSHALLOW` - * to "unshallow" a shallow repository. + * to "unshallow" a shallow repository. Cannot be used in + * conjunction with a specific shallow_since date. * * The default is full (`GIT_FETCH_DEPTH_FULL` or `0`). */ int depth; + /** + * Date from which to fetch history, or 'GIT_FETCH_SINCE_UNSPECIFIED' + * to fetch all history. + * Cannot be used in conjuction with a specific depth. + * + * The default is to fetch all history (`GIT_FETCH_SINCE_UNSPECIFIED` or `-1`). + */ + git_time_t shallow_since; + /** * Whether to allow off-site redirects. If this is not * specified, the `http.followRedirects` configuration setting @@ -837,7 +850,11 @@ typedef struct { GIT_FETCH_PRUNE_UNSPECIFIED, \ GIT_REMOTE_UPDATE_FETCHHEAD, \ GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, \ - GIT_PROXY_OPTIONS_INIT } + GIT_PROXY_OPTIONS_INIT, \ + GIT_FETCH_DEPTH_FULL, \ + GIT_FETCH_SINCE_UNSPECIFIED, \ + GIT_REMOTE_REDIRECT_NONE \ +} /** * Initialize git_fetch_options structure diff --git a/include/git2/sys/transport.h b/include/git2/sys/transport.h index 6a190242cdb..046e5272ffe 100644 --- a/include/git2/sys/transport.h +++ b/include/git2/sys/transport.h @@ -38,6 +38,7 @@ typedef struct { git_oid *shallow_roots; size_t shallow_roots_len; int depth; + git_time_t shallow_since; } git_fetch_negotiation; struct git_transport { diff --git a/src/libgit2/fetch.c b/src/libgit2/fetch.c index 3769f951176..3e834b5c00a 100644 --- a/src/libgit2/fetch.c +++ b/src/libgit2/fetch.c @@ -177,6 +177,7 @@ int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts) if (opts) { GIT_ASSERT_ARG(opts->depth >= 0); remote->nego.depth = opts->depth; + remote->nego.shallow_since = opts->shallow_since; } if (filter_wants(remote, opts) < 0) diff --git a/src/libgit2/transports/local.c b/src/libgit2/transports/local.c index 854390534f2..367da862e00 100644 --- a/src/libgit2/transports/local.c +++ b/src/libgit2/transports/local.c @@ -303,6 +303,12 @@ static int local_negotiate_fetch( git_error_set(GIT_ERROR_NET, "shallow fetch is not supported by the local transport"); return GIT_ENOTSUPPORTED; } + if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + git_error_set( + GIT_ERROR_NET, + "shallow-since fetch is not supported by the local transport"); + return GIT_ENOTSUPPORTED; + } /* Fill in the loids */ git_vector_foreach(&t->refs, i, rhead) { diff --git a/src/libgit2/transports/smart_pkt.c b/src/libgit2/transports/smart_pkt.c index 29ccb83ac72..1436eabd70a 100644 --- a/src/libgit2/transports/smart_pkt.c +++ b/src/libgit2/transports/smart_pkt.c @@ -828,6 +828,14 @@ int git_pkt_buffer_wants( } if (wants->depth > 0) { + + if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + git_error_set( + GIT_ERROR_NET, + "depth and shallow-since must not be used together"); + return GIT_ENOTSUPPORTED; + } + git_str deepen_buf = GIT_STR_INIT; git_str_printf(&deepen_buf, "deepen %d\n", wants->depth); @@ -835,6 +843,25 @@ int git_pkt_buffer_wants( git_str_dispose(&deepen_buf); + if (git_str_oom(buf)) + return -1; + + } else if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + + git_str shallow_since_buf = GIT_STR_INIT; + + /* The git command-line option is "shallow-since", but the protocol uses "deepen-since" */ + git_str_printf( + &shallow_since_buf, "deepen-since %d\n", + wants->shallow_since); + + git_str_printf( + buf, "%04x%s", + (unsigned int)git_str_len(&shallow_since_buf) + 4, + git_str_cstr(&shallow_since_buf)); + + git_str_dispose(&shallow_since_buf); + if (git_str_oom(buf)) return -1; } diff --git a/src/libgit2/transports/smart_protocol.c b/src/libgit2/transports/smart_protocol.c index 87c1904589d..4bf38a6ade8 100644 --- a/src/libgit2/transports/smart_protocol.c +++ b/src/libgit2/transports/smart_protocol.c @@ -435,7 +435,7 @@ int git_smart__negotiate_fetch( if ((error = git_revwalk__push_glob(walk, "refs/*", &opts)) < 0) goto on_error; - if (wants->depth > 0) { + if (wants->depth > 0 || wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { git_pkt_shallow *pkt; if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) diff --git a/tests/libgit2/online/shallow.c b/tests/libgit2/online/shallow.c index 8d71c6be507..75245fb4f2f 100644 --- a/tests/libgit2/online/shallow.c +++ b/tests/libgit2/online/shallow.c @@ -1,6 +1,7 @@ #include "clar_libgit2.h" #include "futils.h" #include "repository.h" +#include "date.h" static int remote_single_branch(git_remote **out, git_repository *repo, const char *name, const char *url, void *payload) { @@ -124,6 +125,135 @@ void test_online_shallow__clone_depth_five(void) git_repository_free(repo); } +void test_online_shallow__clone_since_oldest(void) +{ + git_str path = GIT_STR_INIT; + git_repository *repo; + git_revwalk *walk; + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + git_oid oid; + git_oid *roots; + size_t roots_len; + size_t num_commits = 0; + int error = 0; + + /* Specify a date before the old commit. */ + git_time_t since_date; + cl_git_pass(git_date_parse(&since_date,"2005-04-05")); + clone_opts.fetch_opts.shallow_since = since_date; + clone_opts.remote_cb = remote_single_branch; + + git_str_joinpath(&path, clar_sandbox_path(), "shallowclone_old"); + + cl_git_pass(git_clone( + &repo, "https://github.com/libgit2/TestGitRepository", + git_str_cstr(&path), &clone_opts)); + + cl_assert_equal_b(false, git_repository_is_shallow(repo)); + + cl_git_pass(git_repository__shallow_roots(&roots, &roots_len, repo)); + cl_assert_equal_i(0, roots_len); + + git_revwalk_new(&walk, repo); + + git_revwalk_push_head(walk); + + while ((error = git_revwalk_next(&oid, walk)) == GIT_OK) { + num_commits++; + } + + cl_assert_equal_i(num_commits, 21); + cl_assert_equal_i(error, GIT_ITEROVER); + + git__free(roots); + git_str_dispose(&path); + git_revwalk_free(walk); + git_repository_free(repo); +} + +void test_online_shallow__clone_since_midpoint(void) +{ + git_str path = GIT_STR_INIT; + git_repository *repo; + git_revwalk *walk; + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + git_oid oid; + git_oid *roots; + size_t roots_len; + size_t num_commits = 0; + int error = 0; + + /* Specify a date between the oldest and most recent commit. */ + git_time_t since_date; + cl_git_pass(git_date_parse(&since_date, "2005-04-07 22:30:00 UTC")); + clone_opts.fetch_opts.shallow_since = since_date; + clone_opts.remote_cb = remote_single_branch; + + git_str_joinpath(&path, clar_sandbox_path(), "shallowclone_midpoint"); + + cl_git_pass(git_clone( + &repo, "https://github.com/libgit2/TestGitRepository", + git_str_cstr(&path), &clone_opts)); + + cl_assert_equal_b(true, git_repository_is_shallow(repo)); + + cl_git_pass(git_repository__shallow_roots(&roots, &roots_len, repo)); + cl_assert_equal_i(3, roots_len); + cl_assert_equal_s( + "d0114ab8ac326bab30e3a657a0397578c5a1af88", + git_oid_tostr_s(&roots[0])); + cl_assert_equal_s( + "49322bb17d3acc9146f98c97d078513228bbf3c0", + git_oid_tostr_s(&roots[1])); + cl_assert_equal_s( + "f73b95671f326616d66b2afb3bdfcdbbce110b44", + git_oid_tostr_s(&roots[2])); + + git_revwalk_new(&walk, repo); + + git_revwalk_push_head(walk); + + while ((error = git_revwalk_next(&oid, walk)) == GIT_OK) { + num_commits++; + } + + cl_assert_equal_i(num_commits, 1); + cl_assert_equal_i(error, GIT_ITEROVER); + + git__free(roots); + git_str_dispose(&path); + git_revwalk_free(walk); + git_repository_free(repo); +} + +void test_online_shallow__clone_since_recent(void) +{ + git_str path = GIT_STR_INIT; + git_repository *repo; + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + size_t num_commits = 0; + int error = 0; + + /* Specify a date between the oldest and most recent commit. */ + git_time_t since_date; + cl_git_pass(git_date_parse(&since_date, "2025-04-07 22:30:00 UTC")); + clone_opts.fetch_opts.shallow_since = since_date; + clone_opts.remote_cb = remote_single_branch; + + git_str_joinpath(&path, clar_sandbox_path(), "shallowclone_recent"); + + error = git_clone( + &repo, "https://github.com/libgit2/TestGitRepository", + git_str_cstr(&path), &clone_opts); + + /* Command-line git gives a different (but no more useful) error in this + * case - "error processing shallow info: 4". */ + cl_assert_equal_i(error, GIT_EEOF); + + git_str_dispose(&path); + git_repository_free(repo); +} + void test_online_shallow__unshallow(void) { git_str path = GIT_STR_INIT; From 2058a36ffd66496ba3e077da80cefb37e43da0be Mon Sep 17 00:00:00 2001 From: Malcolm Wood Date: Thu, 19 Jun 2025 16:10:56 +0100 Subject: [PATCH 2/5] Assign the correct "shallow_since" default when no options are specified. --- src/libgit2/fetch.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libgit2/fetch.c b/src/libgit2/fetch.c index 3e834b5c00a..3f458095e60 100644 --- a/src/libgit2/fetch.c +++ b/src/libgit2/fetch.c @@ -178,6 +178,8 @@ int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts) GIT_ASSERT_ARG(opts->depth >= 0); remote->nego.depth = opts->depth; remote->nego.shallow_since = opts->shallow_since; + } else { + remote->nego.shallow_since = GIT_FETCH_SINCE_UNSPECIFIED; } if (filter_wants(remote, opts) < 0) From 385d88f9d4950aa758ffd9e4aebcb37a7720cb22 Mon Sep 17 00:00:00 2001 From: Malcolm Wood Date: Thu, 19 Jun 2025 16:52:22 +0100 Subject: [PATCH 3/5] Fix comments in new unit tests. --- tests/libgit2/online/shallow.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/libgit2/online/shallow.c b/tests/libgit2/online/shallow.c index 75245fb4f2f..391476a759b 100644 --- a/tests/libgit2/online/shallow.c +++ b/tests/libgit2/online/shallow.c @@ -137,7 +137,7 @@ void test_online_shallow__clone_since_oldest(void) size_t num_commits = 0; int error = 0; - /* Specify a date before the old commit. */ + /* Specify a date before the oldest commit. */ git_time_t since_date; cl_git_pass(git_date_parse(&since_date,"2005-04-05")); clone_opts.fetch_opts.shallow_since = since_date; @@ -234,7 +234,7 @@ void test_online_shallow__clone_since_recent(void) size_t num_commits = 0; int error = 0; - /* Specify a date between the oldest and most recent commit. */ + /* Specify a date newer than the most recent commit. */ git_time_t since_date; cl_git_pass(git_date_parse(&since_date, "2025-04-07 22:30:00 UTC")); clone_opts.fetch_opts.shallow_since = since_date; From 9c8a6fa45397afa1dd7393cecf0211e05540310c Mon Sep 17 00:00:00 2001 From: mwood Date: Fri, 20 Jun 2025 13:47:44 +0100 Subject: [PATCH 4/5] Rename detault value of shallow_since - to "FULL", with value zero, for consistency with "depth" - and create a separate enum for it. --- include/git2/remote.h | 15 +++++++++------ src/libgit2/clone.c | 2 +- src/libgit2/fetch.c | 3 +-- src/libgit2/transports/local.c | 2 +- src/libgit2/transports/smart_pkt.c | 6 +++--- src/libgit2/transports/smart_protocol.c | 2 +- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/git2/remote.h b/include/git2/remote.h index 2b7a3a4243b..4e42cdccb31 100644 --- a/include/git2/remote.h +++ b/include/git2/remote.h @@ -763,11 +763,14 @@ typedef enum { /** The fetch should "unshallow" and fetch missing data. */ GIT_FETCH_DEPTH_UNSHALLOW = 2147483647, - - /** No "shallow-since" date specified. */ - GIT_FETCH_SINCE_UNSPECIFIED = -1 } git_fetch_depth_t; +typedef enum { + /** The fetch is "full" (not shallow). This is the default. */ + GIT_FETCH_SINCE_FULL = 0 +} git_fetch_since_t; + + /** * Fetch options structure. * @@ -819,11 +822,11 @@ typedef struct { int depth; /** - * Date from which to fetch history, or 'GIT_FETCH_SINCE_UNSPECIFIED' + * Date from which to fetch history, or 'GIT_FETCH_SINCE_FULL' * to fetch all history. * Cannot be used in conjuction with a specific depth. * - * The default is to fetch all history (`GIT_FETCH_SINCE_UNSPECIFIED` or `-1`). + * The default is to fetch all history (`GIT_FETCH_SINCE_FULL` or `-1`). */ git_time_t shallow_since; @@ -852,7 +855,7 @@ typedef struct { GIT_REMOTE_DOWNLOAD_TAGS_UNSPECIFIED, \ GIT_PROXY_OPTIONS_INIT, \ GIT_FETCH_DEPTH_FULL, \ - GIT_FETCH_SINCE_UNSPECIFIED, \ + GIT_FETCH_SINCE_FULL, \ GIT_REMOTE_REDIRECT_NONE \ } diff --git a/src/libgit2/clone.c b/src/libgit2/clone.c index 237efc0ba7d..5a4ee7061ea 100644 --- a/src/libgit2/clone.c +++ b/src/libgit2/clone.c @@ -619,7 +619,7 @@ static int clone_repo( /* enforce some behavior on fetch */ options.fetch_opts.update_fetchhead = 0; - if (!options.fetch_opts.depth) + if (!options.fetch_opts.depth && !options.fetch_opts.shallow_since) options.fetch_opts.download_tags = GIT_REMOTE_DOWNLOAD_TAGS_ALL; /* Only clone to a new directory or an empty directory */ diff --git a/src/libgit2/fetch.c b/src/libgit2/fetch.c index 3f458095e60..fee5a734825 100644 --- a/src/libgit2/fetch.c +++ b/src/libgit2/fetch.c @@ -64,6 +64,7 @@ static int mark_local(git_remote *remote) depth, we need to ask for it even though the head exists locally. */ if (remote->nego.depth == GIT_FETCH_DEPTH_FULL && + remote->nego.shallow_since == GIT_FETCH_SINCE_FULL && git_odb_exists(odb, &head->oid)) head->local = 1; else @@ -178,8 +179,6 @@ int git_fetch_negotiate(git_remote *remote, const git_fetch_options *opts) GIT_ASSERT_ARG(opts->depth >= 0); remote->nego.depth = opts->depth; remote->nego.shallow_since = opts->shallow_since; - } else { - remote->nego.shallow_since = GIT_FETCH_SINCE_UNSPECIFIED; } if (filter_wants(remote, opts) < 0) diff --git a/src/libgit2/transports/local.c b/src/libgit2/transports/local.c index 367da862e00..8c96ccde1f6 100644 --- a/src/libgit2/transports/local.c +++ b/src/libgit2/transports/local.c @@ -303,7 +303,7 @@ static int local_negotiate_fetch( git_error_set(GIT_ERROR_NET, "shallow fetch is not supported by the local transport"); return GIT_ENOTSUPPORTED; } - if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + if (wants->shallow_since) { git_error_set( GIT_ERROR_NET, "shallow-since fetch is not supported by the local transport"); diff --git a/src/libgit2/transports/smart_pkt.c b/src/libgit2/transports/smart_pkt.c index 1436eabd70a..d89945f525e 100644 --- a/src/libgit2/transports/smart_pkt.c +++ b/src/libgit2/transports/smart_pkt.c @@ -829,11 +829,11 @@ int git_pkt_buffer_wants( if (wants->depth > 0) { - if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + if (wants->shallow_since > 0) { git_error_set( GIT_ERROR_NET, "depth and shallow-since must not be used together"); - return GIT_ENOTSUPPORTED; + return GIT_EINVALID; } git_str deepen_buf = GIT_STR_INIT; @@ -846,7 +846,7 @@ int git_pkt_buffer_wants( if (git_str_oom(buf)) return -1; - } else if (wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + } else if (wants->shallow_since > 0) { git_str shallow_since_buf = GIT_STR_INIT; diff --git a/src/libgit2/transports/smart_protocol.c b/src/libgit2/transports/smart_protocol.c index 4bf38a6ade8..95a3a5b8735 100644 --- a/src/libgit2/transports/smart_protocol.c +++ b/src/libgit2/transports/smart_protocol.c @@ -435,7 +435,7 @@ int git_smart__negotiate_fetch( if ((error = git_revwalk__push_glob(walk, "refs/*", &opts)) < 0) goto on_error; - if (wants->depth > 0 || wants->shallow_since != GIT_FETCH_SINCE_UNSPECIFIED) { + if (wants->depth > 0 || wants->shallow_since > 0) { git_pkt_shallow *pkt; if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) From 80fd2969ba8e1a444d5b477553b784701069ffb1 Mon Sep 17 00:00:00 2001 From: mwood Date: Fri, 20 Jun 2025 16:46:57 +0100 Subject: [PATCH 5/5] Combine some duplicated error handling. --- src/libgit2/transports/local.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libgit2/transports/local.c b/src/libgit2/transports/local.c index 8c96ccde1f6..dd75bdfc949 100644 --- a/src/libgit2/transports/local.c +++ b/src/libgit2/transports/local.c @@ -299,16 +299,10 @@ static int local_negotiate_fetch( GIT_UNUSED(wants); - if (wants->depth) { + if (wants->depth || wants->shallow_since) { git_error_set(GIT_ERROR_NET, "shallow fetch is not supported by the local transport"); return GIT_ENOTSUPPORTED; } - if (wants->shallow_since) { - git_error_set( - GIT_ERROR_NET, - "shallow-since fetch is not supported by the local transport"); - return GIT_ENOTSUPPORTED; - } /* Fill in the loids */ git_vector_foreach(&t->refs, i, rhead) {