From 82ebd47d30da74f24cbbea991f9e09247d219829 Mon Sep 17 00:00:00 2001 From: Friel Date: Wed, 14 Oct 2026 17:42:05 +0000 Subject: [PATCH 1/5] fetch-pack: stop negotiate-only after exhausting haves When no commits are shared, negotiate_using_fetch() receives a valid NAK for all available haves, then sends another request with no haves. A server can answer that empty request with a packfile section, which the client rejects while expecting acknowledgments. Stop before sending another request once the negotiator is exhausted. Keep the first request even when add_haves() reports zero, because it may contain explicit negotiation-include commits. Only server ACKs are returned as common commits; a completed NAK-only exchange returns an empty set successfully. Cover both negotiation algorithms, a first request containing only included commits, and HTTP response cleanup. Keep total_rounds equal to the number of requests sent and balance the round trace regions. Signed-off-by: Friel --- fetch-pack.c | 16 +++++++++++- t/t5702-protocol-v2.sh | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/fetch-pack.c b/fetch-pack.c index 2f64c3d073d890..1a06e2f6a312c9 100644 --- a/fetch-pack.c +++ b/fetch-pack.c @@ -2556,6 +2556,20 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, trace2_data_intmax("negotiate_using_fetch", the_repository, "in_vain", in_vain); + /* + * Previous responses are complete and there are no new haves. + * An empty request could ask the server to send a pack instead. + * Still send the first request: it may contain only explicit + * negotiation-include commits, which add_haves() does not count. + */ + if (!haves_added && negotiation_round > 1) { + trace2_region_leave_printf("negotiate_using_fetch", "round", + the_repository, "%d", + negotiation_round); + negotiation_round--; + break; + } + /* Send request */ packet_buf_flush(&req_buf); if (write_in_full(fd[1], req_buf.buf, req_buf.len) < 0) @@ -2588,7 +2602,7 @@ void negotiate_using_fetch(const struct oid_array *negotiation_restrict_tips, REACH_SCRATCH, 0, min_generation)) last_iteration = 1; - trace2_region_leave_printf("negotiation", "round", + trace2_region_leave_printf("negotiate_using_fetch", "round", the_repository, "%d", negotiation_round); } diff --git a/t/t5702-protocol-v2.sh b/t/t5702-protocol-v2.sh index 92a6d8530809f0..38c81e1fcd3c53 100755 --- a/t/t5702-protocol-v2.sh +++ b/t/t5702-protocol-v2.sh @@ -912,6 +912,46 @@ test_expect_success 'file:// --negotiate-only' ' test_grep "$COMMON" out ' +for algorithm in consecutive skipping +do + test_expect_success "file:// --negotiate-only with no common commits ($algorithm)" ' + test_when_finished "rm -rf unrelated-client unrelated-server" && + git init unrelated-client && + test_commit -C unrelated-client one && + test_commit -C unrelated-client two && + git init --bare unrelated-server && + test_when_finished "rm -f negotiate-packet negotiate-event" && + GIT_TRACE_PACKET="$PWD/negotiate-packet" \ + GIT_TRACE2_EVENT="$PWD/negotiate-event" \ + git -C unrelated-client -c protocol.version=2 \ + -c fetch.negotiationAlgorithm=$algorithm \ + fetch --negotiate-only --negotiation-tip=HEAD \ + "file://$PWD/unrelated-server" >out && + test_must_be_empty out && + test_grep "fetch< NAK" negotiate-packet && + test_grep ! "fetch< ACK" negotiate-packet && + test_grep ! "pack-objects" negotiate-event && + test_grep "\"key\":\"total_rounds\",\"value\":\"1\"" negotiate-event + ' +done + +for algorithm in consecutive skipping +do + test_expect_success "file:// --negotiate-only with only included commits ($algorithm)" ' + setup_negotiate_only server "file://$PWD/server" && + test_when_finished "rm -f negotiate-event" && + GIT_TRACE2_EVENT="$PWD/negotiate-event" \ + git -C client -c protocol.version=2 \ + -c fetch.negotiationAlgorithm=$algorithm \ + fetch --negotiate-only --negotiation-tip=one \ + --negotiation-include=one origin >out && + git -C client rev-parse one >expect && + test_cmp expect out && + test_grep "\"key\":\"haves_added\",\"value\":\"0\"" negotiate-event && + test_grep "\"key\":\"total_rounds\",\"value\":\"1\"" negotiate-event + ' +done + test_expect_success 'file:// --negotiate-only with protocol v0' ' SERVER="server" && URI="file://$(pwd)/server" && @@ -1619,6 +1659,25 @@ test_expect_success 'http:// --negotiate-only' ' test_grep "$COMMON" out ' +test_expect_success 'http:// --negotiate-only with no common commits' ' + test_when_finished "rm -rf unrelated-client \"$HTTPD_DOCUMENT_ROOT_PATH/unrelated-server\"" && + git init unrelated-client && + test_commit -C unrelated-client one && + test_commit -C unrelated-client two && + git init --bare "$HTTPD_DOCUMENT_ROOT_PATH/unrelated-server" && + test_when_finished "rm -f negotiate-packet negotiate-event" && + GIT_TRACE_PACKET="$PWD/negotiate-packet" \ + GIT_TRACE2_EVENT="$PWD/negotiate-event" \ + git -C unrelated-client -c protocol.version=2 fetch \ + --negotiate-only --negotiation-tip=HEAD \ + "$HTTPD_URL/smart/unrelated-server" >out && + test_must_be_empty out && + test_grep "fetch< NAK" negotiate-packet && + test_grep ! "fetch< ACK" negotiate-packet && + test_grep ! "packfile" negotiate-packet && + test_grep "\"key\":\"total_rounds\",\"value\":\"1\"" negotiate-event +' + test_expect_success 'http:// --negotiate-only without wait-for-done support' ' SERVER="server" && URI="$HTTPD_URL/one_time_script/server" && From fac86f9eb2c869c5fab2ce92c369db66452da3ba Mon Sep 17 00:00:00 2001 From: Friel Date: Wed, 14 Oct 2026 17:42:26 +0000 Subject: [PATCH 2/5] send-pack: allow pushes to require successful negotiation A failed push negotiation currently emits a warning and proceeds to construct a pack. Users who rely on negotiation to avoid an expensive pack have no way to require it to succeed. Accept push.negotiate=required and abort when the negotiate-only fetch fails, before sending ref update commands or starting pack-objects. Keep false as the default and true as best effort, including existing boolean spellings and command-line configuration overrides. Successful negotiation can return no common commits. It does not impose a pack-size limit. Unsupported negotiation, transport errors and an interrupted fetch remain failures. Document these distinctions and test that failures start no pack-objects process and leave the target ref unchanged, while common and no-common pushes succeed. Signed-off-by: Friel --- Documentation/config/push.adoc | 15 ++- send-pack.c | 53 ++++++---- t/meson.build | 1 + t/t5556-push-negotiate.sh | 172 +++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 21 deletions(-) create mode 100755 t/t5556-push-negotiate.sh diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc index 28132eedfee6c0..63a1038c227818 100644 --- a/Documentation/config/push.adoc +++ b/Documentation/config/push.adoc @@ -132,7 +132,20 @@ This will result in only b (a and c are cleared). sent by rounds of negotiation in which the client and the server attempt to find commits in common. If `false`, Git will rely solely on the server's ref advertisement to find commits - in common. + in common. Defaults to `false`. ++ +If set to `required`, abort the push before constructing or sending a +packfile if negotiation fails, including when the server or protocol does +not support negotiation. With `true`, a negotiation failure only produces +a warning and the push proceeds. ++ +A successful negotiation need not find any common commits. `required` +does not limit the size of the resulting packfile or change which commits +the server acknowledges. Negotiation is skipped when there are no +negotiation tips, as with a deletion-only push without explicitly configured +tips. To permit a push after a negotiation failure, explicitly use +`git -c push.negotiate=true push`; to skip negotiation, use +`git -c push.negotiate=false push`. `push.useBitmaps`:: If set to `false`, disable use of bitmaps for `git push` even if diff --git a/send-pack.c b/send-pack.c index ae661e7e7401b3..93a6ff51422e19 100644 --- a/send-pack.c +++ b/send-pack.c @@ -409,12 +409,19 @@ static void reject_invalid_nonce(const char *nonce, int len) } } -static void get_commons_through_negotiation(struct repository *r, - const char *url, - const struct string_list *negotiation_include, - const struct string_list *negotiation_restrict, - const struct ref *remote_refs, - struct oid_array *commons) +/* The push.negotiate policy for attempting negotiation and handling failure. */ +enum push_negotiation_mode { + PUSH_NEGOTIATE_DISABLED, + PUSH_NEGOTIATE_ENABLED, + PUSH_NEGOTIATE_REQUIRED, +}; + +static int get_commons_through_negotiation(struct repository *r, + const char *url, + const struct string_list *negotiation_include, + const struct string_list *negotiation_restrict, + const struct ref *remote_refs, + struct oid_array *commons) { struct child_process child = CHILD_PROCESS_INIT; const struct ref *ref; @@ -454,7 +461,7 @@ static void get_commons_through_negotiation(struct repository *r, if (!nr_negotiation) { child_process_clear(&child); - return; + return 0; } if (start_command(&child)) @@ -475,13 +482,7 @@ static void get_commons_through_negotiation(struct repository *r, oid_array_append(commons, &oid); } while (1); - if (finish_command(&child)) { - /* - * The information that push negotiation provides is useful but - * not mandatory. - */ - warning(_("push negotiation failed; proceeding anyway with push")); - } + return finish_command(&child); } int send_pack(struct repository *r, @@ -503,7 +504,8 @@ int send_pack(struct repository *r, int quiet_supported = 0; int agent_supported = 0; int advertise_sid = 0; - int push_negotiate = 0; + enum push_negotiation_mode push_negotiate = PUSH_NEGOTIATE_DISABLED; + const char *push_negotiate_value; int use_atomic = 0; int atomic_supported = 0; int use_push_options = 0; @@ -524,14 +526,25 @@ int send_pack(struct repository *r, goto out; } - repo_config_get_bool(r, "push.negotiate", &push_negotiate); + if (!repo_config_get_value(r, "push.negotiate", &push_negotiate_value)) { + if (push_negotiate_value && + !strcasecmp(push_negotiate_value, "required")) + push_negotiate = PUSH_NEGOTIATE_REQUIRED; + else if (git_config_bool("push.negotiate", push_negotiate_value)) + push_negotiate = PUSH_NEGOTIATE_ENABLED; + } if (push_negotiate) { trace2_region_enter("send_pack", "push_negotiate", r); - get_commons_through_negotiation(r, args->url, - args->negotiation_include, - args->negotiation_restrict, - remote_refs, &commons); + ret = get_commons_through_negotiation(r, args->url, + args->negotiation_include, + args->negotiation_restrict, + remote_refs, &commons); trace2_region_leave("send_pack", "push_negotiate", r); + if (ret) { + if (push_negotiate == PUSH_NEGOTIATE_REQUIRED) + die(_("push negotiation failed; aborting push")); + warning(_("push negotiation failed; proceeding anyway with push")); + } } if (!repo_config_get_bool(r, "push.usebitmaps", &use_bitmaps)) diff --git a/t/meson.build b/t/meson.build index 7f53cca7d1f891..3ddf799453737d 100644 --- a/t/meson.build +++ b/t/meson.build @@ -707,6 +707,7 @@ integration_tests = [ 't5553-set-upstream.sh', 't5554-noop-fetch-negotiator.sh', 't5555-http-smart-common.sh', + 't5556-push-negotiate.sh', 't5557-http-get.sh', 't5558-clone-bundle-uri.sh', 't5559-http-fetch-smart-http2.sh', diff --git a/t/t5556-push-negotiate.sh b/t/t5556-push-negotiate.sh new file mode 100755 index 00000000000000..7218476d7c23ac --- /dev/null +++ b/t/t5556-push-negotiate.sh @@ -0,0 +1,172 @@ +#!/bin/sh + +test_description='required negotiation before constructing a push pack' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +setup_push () { + test_when_finished "rm -rf client server" && + git init client && + test_commit -C client base && + test_commit -C client tip && + git init --bare server && + git -C client remote add origin ../server +} + +setup_common () { + setup_push && + git -C client push origin base:refs/heads/base && + git -C server config receive.hideRefs refs/heads/base +} + +check_no_pack () { + test_grep ! "pack-objects" trace && + test_grep ! "proceeding anyway" err && + test_must_fail git -C server show-ref --verify refs/heads/main +} + +for mode in true required +do + test_expect_success "push.negotiate=$mode finds hidden common history" ' + setup_common && + test_when_finished "rm -f trace" && + GIT_TRACE2_EVENT="$PWD/trace" \ + git -C client -c protocol.version=2 -c push.negotiate=$mode \ + push origin main && + test_grep "write_pack_file/wrote.*\"value\":\"3\"" trace && + git -C client rev-parse main >expect && + git -C server rev-parse main >actual && + test_cmp expect actual + ' +done + +test_expect_success 'required negotiation can succeed without a common commit' ' + setup_push && + test_when_finished "rm -f trace packet" && + GIT_TRACE2_EVENT="$PWD/trace" GIT_TRACE_PACKET="$PWD/packet" \ + git -C client -c protocol.version=2 -c push.negotiate=required \ + push origin main && + test_grep "fetch< NAK" packet && + test_grep ! "fetch< ACK" packet && + test_grep "write_pack_file/wrote.*\"value\":\"6\"" trace && + git -C client rev-parse main >expect && + git -C server rev-parse main >actual && + test_cmp expect actual +' + +for version in 0 1 +do + test_expect_success "required negotiation rejects protocol v$version before packing" ' + setup_common && + test_when_finished "rm -f trace" && + test_must_fail env GIT_TRACE2_EVENT="$PWD/trace" \ + git -C client -c protocol.version=$version \ + -c push.negotiate=required push origin main 2>err && + test_grep "requires protocol v2" err && + test_grep "push negotiation failed; aborting push" err && + check_no_pack + ' +done + +for mode in true false +do + test_expect_success "explicit push.negotiate=$mode overrides required" ' + setup_common && + test_config -C client push.negotiate required && + test_when_finished "rm -f trace" && + GIT_TRACE2_EVENT="$PWD/trace" \ + git -C client -c protocol.version=0 -c push.negotiate=$mode \ + push origin main 2>err && + test_grep "write_pack_file/wrote.*\"value\":\"6\"" trace && + if test "$mode" = true + then + test_grep "push negotiation failed; proceeding anyway" err + else + test_grep ! "push negotiation failed" err + fi + ' +done + +test_expect_success 'push.negotiate preserves boolean spellings' ' + setup_common && + for mode in yes on 1 no off 0 "" + do + git -C client -c push.negotiate="$mode" \ + push --dry-run origin main || return 1 + done && + git -C client -c push.negotiate push --dry-run origin main +' + +test_expect_success 'invalid push.negotiate fails before packing' ' + setup_common && + test_when_finished "rm -f trace" && + test_must_fail env GIT_TRACE2_EVENT="$PWD/trace" \ + git -C client -c push.negotiate=invalid push origin main 2>err && + test_grep "bad boolean config value" err && + check_no_pack +' + +test_expect_success 'deletion-only push does not require negotiation' ' + setup_common && + git -C server config --unset receive.hideRefs && + test_when_finished "rm -f trace" && + GIT_TRACE2_EVENT="$PWD/trace" \ + git -C client -c protocol.version=0 -c push.negotiate=required \ + push origin :base && + test_grep ! "negotiate-only" trace && + test_grep ! "pack-objects" trace && + test_must_fail git -C server show-ref --verify refs/heads/base +' + +setup_ssh_push () { + setup_common && + write_script ssh-wrapper <<-\EOF && + if test "$1" = -o + then + shift 2 + fi + shift + case "$1" in + git-upload-pack*) exec "$NEGOTIATION_SCRIPT" ;; + *) exec sh -c "$1" ;; + esac + EOF + test_config -C client remote.origin.url "testhost:$PWD/server" && + test_config -C client ssh.variant ssh +} + +test_expect_success 'required negotiation stops on transport failure' ' + setup_ssh_push && + write_script failed-upload-pack <<-\EOF && + exit 1 + EOF + test_when_finished "rm -f trace" && + test_must_fail env GIT_TRACE2_EVENT="$PWD/trace" \ + GIT_SSH="$PWD/ssh-wrapper" \ + NEGOTIATION_SCRIPT="$PWD/failed-upload-pack" \ + git -C client -c push.negotiate=required push origin main 2>err && + test_grep "Could not read from remote repository" err && + test_grep "push negotiation failed; aborting push" err && + check_no_pack +' + +test_expect_success !MINGW 'required negotiation stops when negotiation is interrupted' ' + setup_ssh_push && + write_script interrupted-upload-pack <<-\EOF && + # GIT_SSH executes this script directly, so its parent is fetch. + kill -TERM "$PPID" + EOF + test_when_finished "rm -f trace" && + test_must_fail env GIT_TRACE2_EVENT="$PWD/trace" \ + GIT_SSH="$PWD/ssh-wrapper" \ + NEGOTIATION_SCRIPT="$PWD/interrupted-upload-pack" \ + git -C client -c push.negotiate=required push origin main 2>err && + test_grep "\"code\":143" trace && + test_grep "push negotiation failed; aborting push" err && + check_no_pack +' + +test_done From 9e7388372090e56e72ff15e85b61c78131166aad Mon Sep 17 00:00:00 2001 From: Friel Date: Wed, 14 Oct 2026 17:42:26 +0000 Subject: [PATCH 3/5] t: verify required push negotiation stops on HTTP failures Exercise HTTP 400 and 500 responses during the negotiation POST, with successful receive-pack discovery. Best-effort negotiation continues, while required negotiation aborts before pack-objects and leaves the pushed ref absent. Also cover required negotiation success, an unsupported wait-for-done capability, and a malformed acknowledgments section. Reuse the existing one-time response modifier so the fixtures need no new server handler. Signed-off-by: Friel --- t/t5549-fetch-push-http.sh | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/t/t5549-fetch-push-http.sh b/t/t5549-fetch-push-http.sh index 2cdebcb7356332..9b3befdaa11f2c 100755 --- a/t/t5549-fetch-push-http.sh +++ b/t/t5549-fetch-push-http.sh @@ -69,4 +69,87 @@ EOF test_cmp warning-expect warning-actual ' +test_expect_success 'push with required negotiation' ' + setup_client_and_server && + test_when_finished "rm -f event" && + GIT_TRACE2_EVENT="$PWD/event" \ + git -C client -c protocol.version=2 -c push.negotiate=required \ + push "$URI" main:refs/remotes/origin/main && + grep_wrote 3 event +' + +for status in 400 500 +do + for mode in true required + do + test_expect_success "push.negotiate=$mode with HTTP $status during negotiation" ' + setup_client_and_server && + test_when_finished "rm -f event \"$HTTPD_ROOT_PATH/one-time-script\"" && + write_script "$HTTPD_ROOT_PATH/one-time-script" <<-EOF && + if test "\$REQUEST_METHOD" = POST && + test "\${PATH_INFO##*/}" = git-upload-pack + then + printf "Status: $status Test failure\\r\\n" + printf "Content-Type: text/plain\\r\\n\\r\\n" + echo "negotiation failed" + else + cat "\$1" + fi + EOF + if test "$mode" = required + then + test_must_fail env GIT_TRACE2_EVENT="$PWD/event" \ + git -C client -c protocol.version=2 -c push.negotiate=$mode \ + push "$HTTPD_URL/one_time_script/server" \ + main:refs/remotes/origin/main 2>err && + test_grep "push negotiation failed; aborting push" err && + test_grep ! "proceeding anyway" err && + test_grep ! "pack-objects" event && + test_must_fail git -C "$SERVER" show-ref --verify refs/remotes/origin/main + else + GIT_TRACE2_EVENT="$PWD/event" \ + git -C client -c protocol.version=2 -c push.negotiate=$mode \ + push "$HTTPD_URL/one_time_script/server" \ + main:refs/remotes/origin/main 2>err && + test_grep "push negotiation failed; proceeding anyway" err && + grep_wrote 6 event + fi && + test_grep "HTTP $status" err && + test_path_is_missing "$HTTPD_ROOT_PATH/one-time-script" + ' + done +done + +test_expect_success 'required negotiation rejects a missing wait-for-done capability' ' + setup_client_and_server && + test_when_finished "rm -f event \"$HTTPD_ROOT_PATH/one-time-script\"" && + write_script "$HTTPD_ROOT_PATH/one-time-script" <<-\EOF && + sed "s/ wait-for-done/ xxxx-xxx-xxxx/" "$1" + EOF + test_must_fail env GIT_TRACE2_EVENT="$PWD/event" \ + git -C client -c protocol.version=2 -c push.negotiate=required \ + push "$HTTPD_URL/one_time_script/server" \ + main:refs/remotes/origin/main 2>err && + test_grep "server does not support wait-for-done" err && + test_grep "push negotiation failed; aborting push" err && + test_grep ! "pack-objects" event && + test_must_fail git -C "$SERVER" show-ref --verify refs/remotes/origin/main +' + +test_expect_success 'required negotiation rejects an invalid acknowledgments section' ' + setup_client_and_server && + test_when_finished "rm -f event \"$HTTPD_ROOT_PATH/one-time-script\"" && + write_script "$HTTPD_ROOT_PATH/one-time-script" <<-\EOF && + sed "s/acknowledgments/not-ack-section/" "$1" + EOF + test_must_fail env GIT_TRACE2_EVENT="$PWD/event" \ + git -C client -c protocol.version=2 -c push.negotiate=required \ + push "$HTTPD_URL/one_time_script/server" \ + main:refs/remotes/origin/main 2>err && + test_grep "expected .acknowledgments." err && + test_grep "push negotiation failed; aborting push" err && + test_grep ! "pack-objects" event && + test_must_fail git -C "$SERVER" show-ref --verify refs/remotes/origin/main +' + test_done From 530474ae614d198836edc6bd993c2419e4057058 Mon Sep 17 00:00:00 2001 From: Friel Date: Wed, 14 Oct 2026 18:27:49 +0000 Subject: [PATCH 4/5] transport: reject required negotiation when helpers cannot enforce it WebDAV and remote-helper push/export can bypass send_pack(), so checking push.negotiate there alone can still allow an unsupported transport to upload objects. Share the configuration parser and reject required mode before entering http-push or starting fast-export. Check direct http-push before HTTP initialization as well, including mixed deletion/update batches that could otherwise partially update refs. Require push helpers to advertise push-negotiate-required before sending them updates. remote-curl advertises the capability because it now either uses send-pack's negotiation or rejects WebDAV. Successful helper connect takeover continues through native negotiation. Boolean settings retain their existing behavior. Advertise feature: push-negotiate-required in version --build-options so callers can distinguish complete enforcement from a version that ignores the configuration on some transports. Document the helper contract and cover unsupported helpers, connect fallback/takeover, export, and WebDAV with small generic regressions. Signed-off-by: Friel --- Documentation/config/push.adoc | 14 ++- Documentation/git-version.adoc | 5 + Documentation/gitremote-helpers.adoc | 14 +++ help.c | 1 + http-push.c | 4 + remote-curl.c | 4 + send-pack.c | 28 +++-- send-pack.h | 10 ++ t/t5556-push-negotiate.sh | 156 +++++++++++++++++++++++++++ t/t5801-remote-helpers.sh | 23 ++++ transport-helper.c | 14 +++ 11 files changed, 255 insertions(+), 18 deletions(-) diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc index 63a1038c227818..06a3810587b8e7 100644 --- a/Documentation/config/push.adoc +++ b/Documentation/config/push.adoc @@ -139,11 +139,19 @@ packfile if negotiation fails, including when the server or protocol does not support negotiation. With `true`, a negotiation failure only produces a warning and the push proceeds. + +WebDAV and remote-helper `export` do not support required negotiation. +Remote-helper `push` requires the helper to advertise +`push-negotiate-required`; otherwise Git rejects the push before submitting +it to the helper. Helpers that connect to Git's native protocol use the +normal negotiation implementation. See linkgit:gitremote-helpers[7]. ++ A successful negotiation need not find any common commits. `required` does not limit the size of the resulting packfile or change which commits -the server acknowledges. Negotiation is skipped when there are no -negotiation tips, as with a deletion-only push without explicitly configured -tips. To permit a push after a negotiation failure, explicitly use +the server acknowledges. On a supported transport, negotiation is skipped +when there are no negotiation tips, as with a deletion-only push without +explicitly configured tips. Unsupported transports reject updates, including +deletions, without starting object transfer. To permit a push after a +negotiation failure, explicitly use `git -c push.negotiate=true push`; to skip negotiation, use `git -c push.negotiate=false push`. diff --git a/Documentation/git-version.adoc b/Documentation/git-version.adoc index 9462043a142838..5920abf3e99d10 100644 --- a/Documentation/git-version.adoc +++ b/Documentation/git-version.adoc @@ -30,6 +30,11 @@ detection algorithm and thus may be vulnerable to known SHA-1 collision attacks. When a faster SHA-1 implementation without collision detection is used for only non-cryptographic purposes, the algorithm is displayed in the form `non-collision-detecting-SHA-1: