From a8faa7a56033486c576b6386798dca4591e163eb Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 16 Apr 2026 09:20:57 +0000 Subject: [PATCH 01/22] http: extract http_reauth_prepare() from retry paths All three HTTP retry paths (http_request_recoverable, post_rpc, probe_rpc) call credential_fill() directly when handling HTTP_REAUTH. Extract this into a helper function so that a subsequent commit can add pre-fill logic (such as attempting empty-auth before prompting) in one place. No functional change. Signed-off-by: Matthew John Cheetham Signed-off-by: Junio C Hamano --- http.c | 7 ++++++- http.h | 6 ++++++ remote-curl.c | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/http.c b/http.c index 67c9c6fc60673d..f3ba2964b943ef 100644 --- a/http.c +++ b/http.c @@ -665,6 +665,11 @@ static void init_curl_http_auth(CURL *result) } } +void http_reauth_prepare(int all_capabilities) +{ + credential_fill(the_repository, &http_auth, all_capabilities); +} + /* *var must be free-able */ static void var_override(char **var, char *value) { @@ -2398,7 +2403,7 @@ static int http_request_recoverable(const char *url, sleep(retry_delay); } } else if (ret == HTTP_REAUTH) { - credential_fill(the_repository, &http_auth, 1); + http_reauth_prepare(1); } ret = http_request(url, result, target, options); diff --git a/http.h b/http.h index f9ee888c3ed67e..729c51904d39ad 100644 --- a/http.h +++ b/http.h @@ -76,6 +76,12 @@ extern int http_is_verbose; extern ssize_t http_post_buffer; extern struct credential http_auth; +/** + * Prepare for an HTTP re-authentication retry. This fills credentials + * via credential_fill() so the next request can include them. + */ +void http_reauth_prepare(int all_capabilities); + extern char curl_errorstr[CURL_ERROR_SIZE]; enum http_follow_config { diff --git a/remote-curl.c b/remote-curl.c index aba60d571282d3..affdb880f7b3bf 100644 --- a/remote-curl.c +++ b/remote-curl.c @@ -946,7 +946,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece do { err = probe_rpc(rpc, &results); if (err == HTTP_REAUTH) - credential_fill(the_repository, &http_auth, 0); + http_reauth_prepare(0); } while (err == HTTP_REAUTH); if (err != HTTP_OK) return -1; @@ -1068,7 +1068,7 @@ static int post_rpc(struct rpc_state *rpc, int stateless_connect, int flush_rece rpc->any_written = 0; err = run_slot(slot, NULL); if (err == HTTP_REAUTH && !large_request) { - credential_fill(the_repository, &http_auth, 0); + http_reauth_prepare(0); curl_slist_free_all(headers); goto retry; } From 5dbc8c1367226cae6acaa1626d31f01bd186a28c Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 16 Apr 2026 09:20:58 +0000 Subject: [PATCH 02/22] http: attempt Negotiate auth in http.emptyAuth=auto mode When a server advertises Negotiate (SPNEGO) authentication, the "auto" mode of http.emptyAuth should detect this as an "exotic" method and proactively send empty credentials, allowing libcurl to use the system Kerberos ticket without prompting the user. However, two features interact to prevent this from working: The Negotiate-stripping logic, introduced in 4dbe66464b (remote-curl: fall back to Basic auth if Negotiate fails, 2015-01-08), removes CURLAUTH_GSSNEGOTIATE from the allowed methods on the first 401 response. The empty-auth auto-detection, introduced in 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25), then checks the remaining methods for anything "exotic" -- but Negotiate has already been removed, so auto mode never activates for servers whose only non-Basic/Digest method is Negotiate (e.g., Apache with mod_auth_kerb offering Basic + Negotiate). Fix this by delaying the Negotiate stripping in auto mode: on the first 401, keep Negotiate in the allowed methods so that auto mode can detect it and retry with empty credentials. If that attempt fails (no valid Kerberos ticket), strip Negotiate on the second 401 and fall through to credential_fill() as usual. To support this, also teach http_reauth_prepare() to skip credential_fill() when empty auth is about to be attempted, since filling real credentials would bypass the empty-auth mechanism. The true and false modes are unchanged: true sends empty credentials on the very first request (before any 401), and false never sends them. Signed-off-by: Matthew John Cheetham Signed-off-by: Junio C Hamano --- http.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/http.c b/http.c index f3ba2964b943ef..412f7af2504b26 100644 --- a/http.c +++ b/http.c @@ -138,6 +138,7 @@ static unsigned long empty_auth_useless = CURLAUTH_BASIC | CURLAUTH_DIGEST_IE | CURLAUTH_DIGEST; +static int empty_auth_try_negotiate; static struct curl_slist *pragma_header; static struct string_list extra_http_headers = STRING_LIST_INIT_DUP; @@ -667,6 +668,17 @@ static void init_curl_http_auth(CURL *result) void http_reauth_prepare(int all_capabilities) { + /* + * If we deferred stripping Negotiate to give empty auth a + * chance (auto mode), skip credential_fill on this retry so + * that init_curl_http_auth() sends empty credentials and + * libcurl can attempt Negotiate with the system ticket cache. + */ + if (empty_auth_try_negotiate && + !http_auth.password && !http_auth.credential && + (http_auth_methods & CURLAUTH_GSSNEGOTIATE)) + return; + credential_fill(the_repository, &http_auth, all_capabilities); } @@ -1895,7 +1907,18 @@ static int handle_curl_result(struct slot_results *results) http_proactive_auth = PROACTIVE_AUTH_NONE; return HTTP_NOAUTH; } else { - http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; + if (curl_empty_auth == -1 && + !empty_auth_try_negotiate && + (results->auth_avail & CURLAUTH_GSSNEGOTIATE)) { + /* + * In auto mode, give Negotiate a chance via + * empty auth before stripping it. If it fails, + * we will strip it on the next 401. + */ + empty_auth_try_negotiate = 1; + } else { + http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; + } if (results->auth_avail) { http_auth_methods &= results->auth_avail; http_auth_methods_restricted = 1; From 9b1630b97273beceb64ea8f740c5820317aaa8b3 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 16 Apr 2026 09:20:59 +0000 Subject: [PATCH 03/22] t5563: add tests for http.emptyAuth with Negotiate Add tests exercising the interaction between http.emptyAuth and servers that advertise Negotiate (SPNEGO) authentication. Verify that auto mode gives Negotiate a chance via empty auth (resulting in two 401 responses before falling through to credential_fill with Basic credentials), and that false mode strips Negotiate immediately (only one 401 response). Signed-off-by: Matthew John Cheetham Signed-off-by: Junio C Hamano --- t/t5563-simple-http-auth.sh | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh index 00635816156ba3..a7d475dd68dbd7 100755 --- a/t/t5563-simple-http-auth.sh +++ b/t/t5563-simple-http-auth.sh @@ -719,4 +719,78 @@ test_expect_success 'access using three-legged auth' ' EOF ' +test_lazy_prereq SPNEGO 'curl --version | grep -qi "SPNEGO\|GSS-API\|Kerberos\|negotiate"' + +test_expect_success SPNEGO 'http.emptyAuth=auto attempts Negotiate before credential_fill' ' + test_when_finished "per_test_cleanup" && + + set_credential_reply get <<-EOF && + username=alice + password=secret-passwd + EOF + + # Basic base64(alice:secret-passwd) + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== + EOF + + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && + id=1 status=200 + id=default response=WWW-Authenticate: Negotiate + id=default response=WWW-Authenticate: Basic realm="example.com" + EOF + + test_config_global credential.helper test-helper && + GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-auto" \ + git -c http.emptyAuth=auto \ + ls-remote "$HTTPD_URL/custom_auth/repo.git" && + + # In auto mode with a Negotiate+Basic server, there should be + # three 401 responses: (1) initial no-auth request, (2) empty-auth + # retry where Negotiate fails (no Kerberos ticket), (3) libcurl + # internal Negotiate retry. The fourth attempt uses Basic + # credentials from credential_fill and succeeds. + grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-auto" >actual_401s && + test_line_count = 3 actual_401s && + + expect_credential_query get <<-EOF + capability[]=authtype + capability[]=state + protocol=http + host=$HTTPD_DEST + wwwauth[]=Negotiate + wwwauth[]=Basic realm="example.com" + EOF +' + +test_expect_success SPNEGO 'http.emptyAuth=false skips Negotiate' ' + test_when_finished "per_test_cleanup" && + + set_credential_reply get <<-EOF && + username=alice + password=secret-passwd + EOF + + # Basic base64(alice:secret-passwd) + cat >"$HTTPD_ROOT_PATH/custom-auth.valid" <<-EOF && + id=1 creds=Basic YWxpY2U6c2VjcmV0LXBhc3N3ZA== + EOF + + cat >"$HTTPD_ROOT_PATH/custom-auth.challenge" <<-EOF && + id=1 status=200 + id=default response=WWW-Authenticate: Negotiate + id=default response=WWW-Authenticate: Basic realm="example.com" + EOF + + test_config_global credential.helper test-helper && + GIT_TRACE_CURL="$TRASH_DIRECTORY/trace-false" \ + git -c http.emptyAuth=false \ + ls-remote "$HTTPD_URL/custom_auth/repo.git" && + + # With emptyAuth=false, Negotiate is stripped immediately and + # credential_fill is called right away. Only one 401 response. + grep "HTTP/[0-9.]* 401" "$TRASH_DIRECTORY/trace-false" >actual_401s && + test_line_count = 1 actual_401s +' + test_done From 890229b3f3e635ff4dd9e9e7a3d95a4ac6e5e173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SZEDER=20G=C3=A1bor?= Date: Tue, 21 Apr 2026 21:21:32 +0200 Subject: [PATCH 04/22] t6112: avoid tilde expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e987df5fe6 (list-objects-filter: implement composite filters, 2019-06-27) introduced a test to "t6112-rev-list-filters-objects.sh" that checks the output of a Git command with the following commands: grep ~$omitted_1 actual && grep ~$omitted_2 actual && grep ~$omitted_3 actual && Since the leading tilde in the pattern is not quoted/escaped, it is subject to tilde expansion. So if the system has a user whose username happens to be "$omitted_1", then "grep" would look for that user's home directory. Quote those words starting with a tilde to avoid this. Signed-off-by: SZEDER Gábor Signed-off-by: Junio C Hamano --- t/t6112-rev-list-filters-objects.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/t/t6112-rev-list-filters-objects.sh b/t/t6112-rev-list-filters-objects.sh index 0387f35a326d74..668e56bfccd07c 100755 --- a/t/t6112-rev-list-filters-objects.sh +++ b/t/t6112-rev-list-filters-objects.sh @@ -627,9 +627,9 @@ test_expect_success 'verify collecting omits in combined: filter' ' omitted_2=$(echo a | git hash-object --stdin) && omitted_3=$(echo abcde | git hash-object --stdin) && - grep ~$omitted_1 actual && - grep ~$omitted_2 actual && - grep ~$omitted_3 actual && + grep "~$omitted_1" actual && + grep "~$omitted_2" actual && + grep "~$omitted_3" actual && test_line_count = 3 actual ' From b7b8449e5ae7c6a0bb3e87c82d9dd6fd382baf62 Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:10 +0000 Subject: [PATCH 05/22] xdiff/xdl_cleanup_records: delete local recs pointer Simplify the first 2 for loops by directly indexing the xdfile.recs. recs is unused in the last 2 for loops, remove it. Best viewed with --color-words. Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xprepare.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index cd4fc405eb18fe..d6e1901d2d01c9 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -269,7 +269,6 @@ static bool xdl_clean_mmatch(uint8_t const *action, long i, long s, long e) { */ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xdf2) { long i, nm, mlim; - xrecord_t *recs; xdlclass_t *rcrec; uint8_t *action1 = NULL, *action2 = NULL; bool need_min = !!(cf->flags & XDF_NEED_MINIMAL); @@ -293,16 +292,18 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd */ if ((mlim = xdl_bogosqrt((long)xdf1->nrec)) > XDL_MAX_EQLIMIT) mlim = XDL_MAX_EQLIMIT; - for (i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart]; i <= xdf1->dend; i++, recs++) { - rcrec = cf->rcrecs[recs->minimal_perfect_hash]; + for (i = xdf1->dstart; i <= xdf1->dend; i++) { + size_t mph1 = xdf1->recs[i].minimal_perfect_hash; + rcrec = cf->rcrecs[mph1]; nm = rcrec ? rcrec->len2 : 0; action1[i] = (nm == 0) ? DISCARD: (nm >= mlim && !need_min) ? INVESTIGATE: KEEP; } if ((mlim = xdl_bogosqrt((long)xdf2->nrec)) > XDL_MAX_EQLIMIT) mlim = XDL_MAX_EQLIMIT; - for (i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart]; i <= xdf2->dend; i++, recs++) { - rcrec = cf->rcrecs[recs->minimal_perfect_hash]; + for (i = xdf2->dstart; i <= xdf2->dend; i++) { + size_t mph2 = xdf2->recs[i].minimal_perfect_hash; + rcrec = cf->rcrecs[mph2]; nm = rcrec ? rcrec->len1 : 0; action2[i] = (nm == 0) ? DISCARD: (nm >= mlim && !need_min) ? INVESTIGATE: KEEP; } @@ -312,8 +313,7 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd * false, or become true. */ xdf1->nreff = 0; - for (i = xdf1->dstart, recs = &xdf1->recs[xdf1->dstart]; - i <= xdf1->dend; i++, recs++) { + for (i = xdf1->dstart; i <= xdf1->dend; i++) { if (action1[i] == KEEP || (action1[i] == INVESTIGATE && !xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend))) { xdf1->reference_index[xdf1->nreff++] = i; @@ -324,8 +324,7 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd } xdf2->nreff = 0; - for (i = xdf2->dstart, recs = &xdf2->recs[xdf2->dstart]; - i <= xdf2->dend; i++, recs++) { + for (i = xdf2->dstart; i <= xdf2->dend; i++) { if (action2[i] == KEEP || (action2[i] == INVESTIGATE && !xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend))) { xdf2->reference_index[xdf2->nreff++] = i; From c37f8cda0525d7041d9a22e477117a2962f9f675 Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:11 +0000 Subject: [PATCH 06/22] xdiff: use unambiguous types in xdl_bogo_sqrt() There is no real square root for a negative number and size_t may not be large enough for certain applications, replace long with uint64_t. Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xdiffi.c | 2 +- xdiff/xprepare.c | 4 ++-- xdiff/xutils.c | 4 ++-- xdiff/xutils.h | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/xdiff/xdiffi.c b/xdiff/xdiffi.c index 4376f943dba539..88708c12a3299a 100644 --- a/xdiff/xdiffi.c +++ b/xdiff/xdiffi.c @@ -348,7 +348,7 @@ int xdl_do_diff(mmfile_t *mf1, mmfile_t *mf2, xpparam_t const *xpp, kvdf += xe->xdf2.nreff + 1; kvdb += xe->xdf2.nreff + 1; - xenv.mxcost = xdl_bogosqrt(ndiags); + xenv.mxcost = (long)xdl_bogosqrt((uint64_t)ndiags); if (xenv.mxcost < XDL_MAX_COST_MIN) xenv.mxcost = XDL_MAX_COST_MIN; xenv.snake_cnt = XDL_SNAKE_CNT; diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index d6e1901d2d01c9..48fb5ce6fe6f68 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -290,7 +290,7 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd /* * Initialize temporary arrays with DISCARD, KEEP, or INVESTIGATE. */ - if ((mlim = xdl_bogosqrt((long)xdf1->nrec)) > XDL_MAX_EQLIMIT) + if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf1->nrec)) > XDL_MAX_EQLIMIT) mlim = XDL_MAX_EQLIMIT; for (i = xdf1->dstart; i <= xdf1->dend; i++) { size_t mph1 = xdf1->recs[i].minimal_perfect_hash; @@ -299,7 +299,7 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd action1[i] = (nm == 0) ? DISCARD: (nm >= mlim && !need_min) ? INVESTIGATE: KEEP; } - if ((mlim = xdl_bogosqrt((long)xdf2->nrec)) > XDL_MAX_EQLIMIT) + if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf2->nrec)) > XDL_MAX_EQLIMIT) mlim = XDL_MAX_EQLIMIT; for (i = xdf2->dstart; i <= xdf2->dend; i++) { size_t mph2 = xdf2->recs[i].minimal_perfect_hash; diff --git a/xdiff/xutils.c b/xdiff/xutils.c index 77ee1ad9c86875..9a999acdc079d2 100644 --- a/xdiff/xutils.c +++ b/xdiff/xutils.c @@ -23,8 +23,8 @@ #include "xinclude.h" -long xdl_bogosqrt(long n) { - long i; +uint64_t xdl_bogosqrt(uint64_t n) { + uint64_t i; /* * Classical integer square root approximation using shifts. diff --git a/xdiff/xutils.h b/xdiff/xutils.h index 615b4a9d355433..58f9d74cda37a3 100644 --- a/xdiff/xutils.h +++ b/xdiff/xutils.h @@ -25,7 +25,7 @@ -long xdl_bogosqrt(long n); +uint64_t xdl_bogosqrt(uint64_t n); int xdl_emit_diffrec(char const *rec, long size, char const *pre, long psize, xdemitcb_t *ecb); int xdl_cha_init(chastore_t *cha, long isize, long icount); From 216802587ef8505568e533788d06015c0bfc6fef Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:12 +0000 Subject: [PATCH 07/22] xdiff/xdl_cleanup_records: use unambiguous types Change the parameters of xdl_clean_mmatch() and the local variables i, nm, mlim in xdl_cleanup_records() to use unambiguous types. Best viewed with --color-words. Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xprepare.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index 48fb5ce6fe6f68..386668a92d7358 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -197,8 +197,8 @@ void xdl_free_env(xdfenv_t *xe) { } -static bool xdl_clean_mmatch(uint8_t const *action, long i, long s, long e) { - long r, rdis0, rpdis0, rdis1, rpdis1; +static bool xdl_clean_mmatch(uint8_t const *action, ptrdiff_t i, ptrdiff_t s, ptrdiff_t e) { + ptrdiff_t r, rdis0, rpdis0, rdis1, rpdis1; /* * Limits the window that is examined during the similar-lines @@ -268,7 +268,7 @@ static bool xdl_clean_mmatch(uint8_t const *action, long i, long s, long e) { * might be potentially discarded if they appear in a run of discardable. */ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xdf2) { - long i, nm, mlim; + ptrdiff_t i, nm, mlim; xdlclass_t *rcrec; uint8_t *action1 = NULL, *action2 = NULL; bool need_min = !!(cf->flags & XDF_NEED_MINIMAL); From f99a023df2c3024ccfedc4e1259d5b0732154461 Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:13 +0000 Subject: [PATCH 08/22] xdiff/xdl_cleanup_records: make limits more clear Make the handling of per-file limits and the minimal-case clearer. * Use explicit per-file limit variables (mlim1, mlim2) and initialize them. * The additional condition `!need_min` is redudant now, remove it. Best viewed with --color-words. Helped-by: Phillip Wood Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xprepare.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index 386668a92d7358..7141dbc058f4ae 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -268,7 +268,7 @@ static bool xdl_clean_mmatch(uint8_t const *action, ptrdiff_t i, ptrdiff_t s, pt * might be potentially discarded if they appear in a run of discardable. */ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xdf2) { - ptrdiff_t i, nm, mlim; + ptrdiff_t i, nm, mlim1, mlim2; xdlclass_t *rcrec; uint8_t *action1 = NULL, *action2 = NULL; bool need_min = !!(cf->flags & XDF_NEED_MINIMAL); @@ -290,22 +290,34 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd /* * Initialize temporary arrays with DISCARD, KEEP, or INVESTIGATE. */ - if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf1->nrec)) > XDL_MAX_EQLIMIT) - mlim = XDL_MAX_EQLIMIT; + if (need_min) { + /* i.e. infinity */ + mlim1 = PTRDIFF_MAX; + } else { + mlim1 = xdl_bogosqrt((uint64_t)xdf1->nrec); + if (mlim1 > XDL_MAX_EQLIMIT) + mlim1 = XDL_MAX_EQLIMIT; + } for (i = xdf1->dstart; i <= xdf1->dend; i++) { size_t mph1 = xdf1->recs[i].minimal_perfect_hash; rcrec = cf->rcrecs[mph1]; nm = rcrec ? rcrec->len2 : 0; - action1[i] = (nm == 0) ? DISCARD: (nm >= mlim && !need_min) ? INVESTIGATE: KEEP; + action1[i] = (nm == 0) ? DISCARD: nm >= mlim1 ? INVESTIGATE: KEEP; } - if ((mlim = (long)xdl_bogosqrt((uint64_t)xdf2->nrec)) > XDL_MAX_EQLIMIT) - mlim = XDL_MAX_EQLIMIT; + if (need_min) { + /* i.e. infinity */ + mlim2 = PTRDIFF_MAX; + } else { + mlim2 = xdl_bogosqrt((uint64_t)xdf2->nrec); + if (mlim2 > XDL_MAX_EQLIMIT) + mlim2 = XDL_MAX_EQLIMIT; + } for (i = xdf2->dstart; i <= xdf2->dend; i++) { size_t mph2 = xdf2->recs[i].minimal_perfect_hash; rcrec = cf->rcrecs[mph2]; nm = rcrec ? rcrec->len1 : 0; - action2[i] = (nm == 0) ? DISCARD: (nm >= mlim && !need_min) ? INVESTIGATE: KEEP; + action2[i] = (nm == 0) ? DISCARD: nm >= mlim2 ? INVESTIGATE: KEEP; } /* From c355f69cda6d2df4972131b77dc0702d82b29d8b Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:14 +0000 Subject: [PATCH 09/22] xdiff/xdl_cleanup_records: make setting action easier to follow Rewrite nested ternaries with a clear if/else ladder for action1/action2 to improve readability while preserving behavior. Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xprepare.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index 7141dbc058f4ae..ddd05776761d2c 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -302,7 +302,12 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd size_t mph1 = xdf1->recs[i].minimal_perfect_hash; rcrec = cf->rcrecs[mph1]; nm = rcrec ? rcrec->len2 : 0; - action1[i] = (nm == 0) ? DISCARD: nm >= mlim1 ? INVESTIGATE: KEEP; + if (nm == 0) + action1[i] = DISCARD; + else if (nm < mlim1) + action1[i] = KEEP; + else /* nm >= mlim1 */ + action1[i] = INVESTIGATE; } if (need_min) { @@ -317,7 +322,12 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd size_t mph2 = xdf2->recs[i].minimal_perfect_hash; rcrec = cf->rcrecs[mph2]; nm = rcrec ? rcrec->len1 : 0; - action2[i] = (nm == 0) ? DISCARD: nm >= mlim2 ? INVESTIGATE: KEEP; + if (nm == 0) + action2[i] = DISCARD; + else if (nm < mlim2) + action2[i] = KEEP; + else /* nm >= mlim2 */ + action2[i] = INVESTIGATE; } /* From f87808b7014cf06db4a7e19b193cf9aa7e965ebc Mon Sep 17 00:00:00 2001 From: Ezekiel Newren Date: Wed, 29 Apr 2026 22:08:15 +0000 Subject: [PATCH 10/22] xdiff/xdl_cleanup_records: make execution of action easier to follow Helped-by: Phillip Wood Signed-off-by: Ezekiel Newren Signed-off-by: Junio C Hamano --- xdiff/xprepare.c | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/xdiff/xprepare.c b/xdiff/xprepare.c index ddd05776761d2c..beef711067b612 100644 --- a/xdiff/xprepare.c +++ b/xdiff/xprepare.c @@ -336,24 +336,44 @@ static int xdl_cleanup_records(xdlclassifier_t *cf, xdfile_t *xdf1, xdfile_t *xd */ xdf1->nreff = 0; for (i = xdf1->dstart; i <= xdf1->dend; i++) { - if (action1[i] == KEEP || - (action1[i] == INVESTIGATE && !xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend))) { + uint8_t action = action1[i]; + + if (action == INVESTIGATE) { + if (!xdl_clean_mmatch(action1, i, xdf1->dstart, xdf1->dend)) + action = KEEP; + else + action = DISCARD; + } + + if (action == KEEP) { xdf1->reference_index[xdf1->nreff++] = i; - /* changed[i] remains false, i.e. keep */ - } else + /* changed[i] remains false */ + } else if (action == DISCARD) { xdf1->changed[i] = true; - /* i.e. discard */ + } else { + BUG("Illegal state for action"); + } } xdf2->nreff = 0; for (i = xdf2->dstart; i <= xdf2->dend; i++) { - if (action2[i] == KEEP || - (action2[i] == INVESTIGATE && !xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend))) { + uint8_t action = action2[i]; + + if (action == INVESTIGATE) { + if (!xdl_clean_mmatch(action2, i, xdf2->dstart, xdf2->dend)) + action = KEEP; + else + action = DISCARD; + } + + if (action == KEEP) { xdf2->reference_index[xdf2->nreff++] = i; - /* changed[i] remains false, i.e. keep */ - } else + /* changed[i] remains false */ + } else if (action == DISCARD) { xdf2->changed[i] = true; - /* i.e. discard */ + } else { + BUG("Illegal state for action"); + } } cleanup: From 4919938d284f48b21f8e778c670b1331d1407dbc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 30 Apr 2026 10:54:32 +0000 Subject: [PATCH 11/22] doc: clarify http.emptyAuth values The existing description of http.emptyAuth explains the purpose of the setting but never says what values it accepts. Readers have to infer from context (or read the source) that it takes 'true', 'false', or 'auto', and what each one means. Document the three accepted values explicitly: * 'auto' (the default) only sends empty credentials when the server's 401 response advertises a mechanism that requires them, such as GSS-Negotiate. This matches the long-standing auto-detection behaviour added in 40a18fc77c (http: add an "auto" mode for http.emptyauth, 2017-02-25). * 'true' unconditionally sends empty credentials on the very first request, before any 401 response, for callers that know they want this behaviour up front. * 'false' disables the feature entirely; mechanisms that depend on empty credentials, such as GSS-Negotiate, will not work in this mode. Signed-off-by: Matthew John Cheetham Signed-off-by: Junio C Hamano --- Documentation/config/http.adoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Documentation/config/http.adoc b/Documentation/config/http.adoc index 849c89f36c5ad8..792a71b41350d4 100644 --- a/Documentation/config/http.adoc +++ b/Documentation/config/http.adoc @@ -59,7 +59,18 @@ http.emptyAuth:: Attempt authentication without seeking a username or password. This can be used to attempt GSS-Negotiate authentication without specifying a username in the URL, as libcurl normally requires a username for - authentication. + authentication. Possible values are: ++ +-- +* `auto` (default) - Send empty credentials only if the server's 401 response + advertises an authentication mechanism that requires them (such as + GSS-Negotiate); otherwise fall back to prompting via the credential helper. +* `true` - Always send empty credentials on the very first request, before + receiving any 401 response from the server. +* `false` - Never send empty credentials. Mechanisms that require + empty credentials or an explicit username, such as GSS-Negotiate, will not + work. +-- http.proactiveAuth:: Attempt authentication without first making an unauthenticated attempt and From ab9753e7bc473fabdea46b772563530d6a5230a8 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Tue, 5 May 2026 21:46:38 +0200 Subject: [PATCH 12/22] doc: restore: remove double underscore 69666e67 (doc: convert git-restore to new style format, 2025-01-10) converted `A` to ___; the extra underscore was a mistake. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-restore.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/git-restore.adoc b/Documentation/git-restore.adoc index 751f01b4418b5b..52083d7c6c8da7 100644 --- a/Documentation/git-restore.adoc +++ b/Documentation/git-restore.adoc @@ -43,7 +43,7 @@ given, otherwise from the index. + As a special case, you may use `"..."` as a shortcut for the merge base of __ and __ if there is exactly one merge base. You can -leave out at most one of ___ and __, in which case it defaults to `HEAD`. +leave out at most one of __ and __, in which case it defaults to `HEAD`. `-p`:: `--patch`:: From 48c855bb8f17b86188a704cf8c0c5b4f6a316ade Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Thu, 7 May 2026 21:42:28 +0200 Subject: [PATCH 13/22] doc: add caveat about turning off commit-graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc `technical/commit-graph.adoc` says that replace objects and commit grafts turn off commit-graph: Commit grafts and replace objects can change the shape of the commit history. The latter can also be enabled/disabled on the fly using `--no-replace-objects`. This leads to difficulty storing both possible interpretations of a commit id, especially when computing generation numbers. The commit-graph will not be read or written when replace-objects or grafts are present. But this isn’t mentioned in the user-facing doc. Let’s mention it on git-replace(1) and git-commit-graph(1). Acked-by: Derrick Stolee Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- Documentation/git-commit-graph.adoc | 6 ++++++ Documentation/git-replace.adoc | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/Documentation/git-commit-graph.adoc b/Documentation/git-commit-graph.adoc index 6d19026035f96a..f2a37e91634442 100644 --- a/Documentation/git-commit-graph.adoc +++ b/Documentation/git-commit-graph.adoc @@ -146,6 +146,12 @@ $ git show-ref -s | git commit-graph write --stdin-commits $ git rev-parse HEAD | git commit-graph write --stdin-commits --append ------------------------------------------------ +CAVEATS +------- + +The existence of replace objects or commit grafts turns off reading or +writing to the commit-graph. See linkgit:git-replace[1]. + CONFIGURATION ------------- diff --git a/Documentation/git-replace.adoc b/Documentation/git-replace.adoc index 0a65460adbded5..436a0e58caf0af 100644 --- a/Documentation/git-replace.adoc +++ b/Documentation/git-replace.adoc @@ -145,6 +145,13 @@ commit instead of the replaced commit. There may be other problems when using 'git rev-list' related to pending objects. +CAVEATS +------- + +The existence of replace objects or commit grafts turns off reading or +writing to the commit-graph, which can cause performance issues. See +linkgit:git-commit-graph[1]. + SEE ALSO -------- linkgit:git-hash-object[1] From aa45a5902f23975b7075c97ad0451f7b4e9ccd9a Mon Sep 17 00:00:00 2001 From: Saagar Jha Date: Sun, 10 May 2026 03:50:22 +0000 Subject: [PATCH 14/22] submodule-config: fix reading submodule.fetchJobs update_clone_config_from_gitmodules() passes &max_jobs to config_from_gitmodules(), but max_jobs is already a pointer. This causes the config value to be written to the wrong address and get dropped. Pass max_jobs directly. Signed-off-by: Saagar Jha Signed-off-by: Junio C Hamano --- submodule-config.c | 2 +- t/t7406-submodule-update.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/submodule-config.c b/submodule-config.c index 1f19fe207741bc..57b190678e8de7 100644 --- a/submodule-config.c +++ b/submodule-config.c @@ -1037,5 +1037,5 @@ static int gitmodules_update_clone_config(const char *var, const char *value, void update_clone_config_from_gitmodules(int *max_jobs) { - config_from_gitmodules(gitmodules_update_clone_config, the_repository, &max_jobs); + config_from_gitmodules(gitmodules_update_clone_config, the_repository, max_jobs); } diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh index 3adab12091a5f0..6abb00876a3372 100755 --- a/t/t7406-submodule-update.sh +++ b/t/t7406-submodule-update.sh @@ -1055,6 +1055,14 @@ test_expect_success 'submodule update can be run in parallel' ' ) ' +test_expect_success 'submodule update honors fetch jobs config from .gitmodules' ' + test_when_finished "rm -rf super3" && + git clone cloned super3 && + git -C super3 config -f .gitmodules submodule.fetchJobs 67 && + GIT_TRACE="$(pwd)/trace.out" git -C super3 submodule update --init && + test_grep "67 tasks" trace.out +' + test_expect_success 'git clone passes the parallel jobs config on to submodules' ' test_when_finished "rm -rf super4" && GIT_TRACE=$(pwd)/trace.out git clone --recurse-submodules --jobs 7 . super4 && From 31e8fcabd8a75b2c27cd4c98ea694c6836c29ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Sun, 10 May 2026 14:42:04 +0200 Subject: [PATCH 15/22] sideband: clear full line when printing remote messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit demultiplex_sideband() can write its remote output over active local progress lines. That's why it has been using ANSI code Erase in Line on smart terminals to clear the remainder of lines it writes since ebe8fa738d (fix display overlap between remote and local progress, 2007-11-04). This erases the last character of remote lines that span the full width of the terminal, though, as the cursor is stuck at the rightmost column for them. It's the same effect as in the following command, which clears the 1 and shows just the leading zeros: $ EL="\033[K" $ printf "%0${COLUMNS}d${EL}\n" 1 If we move the ANSI code to the start we get to see the 1 as well: $ printf "${EL}%0${COLUMNS}d\n" 1 So do the same in demultiplex_sideband() and emit the ANSI code as a prefix instead of a suffix to show messages in full even if they happen to fill the whole width of a smart terminal. Reported-by: Hugo Osvaldo Barrera Suggested-by: Chris Torek Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- sideband.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/sideband.c b/sideband.c index ea7c25211ef7e1..48ed4c80997d68 100644 --- a/sideband.c +++ b/sideband.c @@ -120,7 +120,7 @@ static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n) #define DISPLAY_PREFIX "remote: " -#define ANSI_SUFFIX "\033[K" +#define ANSI_PREFIX "\033[K" #define DUMB_SUFFIX " " int demultiplex_sideband(const char *me, int status, @@ -129,15 +129,18 @@ int demultiplex_sideband(const char *me, int status, struct strbuf *scratch, enum sideband_type *sideband_type) { - static const char *suffix; + static const char *prefix, *suffix; const char *b, *brk; int band; if (!suffix) { - if (isatty(2) && !is_terminal_dumb()) - suffix = ANSI_SUFFIX; - else + if (isatty(2) && !is_terminal_dumb()) { + prefix = ANSI_PREFIX DISPLAY_PREFIX; + suffix = ""; + } else { + prefix = DISPLAY_PREFIX; suffix = DUMB_SUFFIX; + } } if (status == PACKET_READ_EOF) { @@ -171,8 +174,7 @@ int demultiplex_sideband(const char *me, int status, case 3: if (die_on_error) die(_("remote error: %s"), buf + 1); - strbuf_addf(scratch, "%s%s", scratch->len ? "\n" : "", - DISPLAY_PREFIX); + strbuf_addf(scratch, "%s%s", scratch->len ? "\n" : "", prefix); maybe_colorize_sideband(scratch, buf + 1, len); *sideband_type = SIDEBAND_REMOTE_ERROR; @@ -203,7 +205,7 @@ int demultiplex_sideband(const char *me, int status, strbuf_addstr(scratch, suffix); if (!scratch->len) - strbuf_addstr(scratch, DISPLAY_PREFIX); + strbuf_addstr(scratch, prefix); /* * A use case that we should not add clear-to-eol suffix @@ -229,8 +231,8 @@ int demultiplex_sideband(const char *me, int status, } if (*b) { - strbuf_addstr(scratch, scratch->len ? - "" : DISPLAY_PREFIX); + if (!scratch->len) + strbuf_addstr(scratch, prefix); maybe_colorize_sideband(scratch, b, strlen(b)); } return 0; From 106b6885c7bbaafc863dff0bb5361f906545de5c Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 10 May 2026 15:41:11 -0700 Subject: [PATCH 16/22] rebase: ignore non-branch update-refs The following Git configuration breaks git rebase --update-refs: [rebase] instructionFormat = %s%d The '%d' format requests all available decorations for a commit, filling the global decoration table with all of them, which --update-refs then uses to populate 'update-ref' instructions in the rebase todo list. Specifically, this results in the following instruction: update-ref HEAD The todo parser then rejects the instruction: error: update-ref requires a fully qualified refname e.g. refs/heads/HEAD error: invalid line 3: update-ref HEAD To fix, ignore decorations that are not local branches when scanning through the table. This matches the documented contract: it moves branch refs under refs/heads/ and leaves display-only decorations (HEAD, tags, etc.) alone. Verification: A regression test that fails without this fix is included. Signed-off-by: Abhinav Gupta Signed-off-by: Junio C Hamano --- sequencer.c | 8 +++++++- t/t3404-rebase-interactive.sh | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 1f492f8460e237..df5906a64e2f0c 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6361,8 +6361,14 @@ static int add_decorations_to_list(const struct commit *commit, /* * If the branch is the current HEAD, then it will be * updated by the default rebase behavior. + * Exclude it from the list of refs to update, + * as well as any non-branch decorations. + * Non-branch decorations may be present if the pretty format + * includes "%d", which would have loaded all refs + * into the global decoration table. */ - if (head_ref && !strcmp(head_ref, decoration->name)) { + if ((head_ref && !strcmp(head_ref, decoration->name)) || + (decoration->type != DECORATION_REF_LOCAL)) { decoration = decoration->next; continue; } diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index e778dd8ae4a6dc..217184fb868330 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1954,6 +1954,24 @@ test_expect_success '--update-refs adds commands with --rebase-merges' ' ) ' +test_expect_success '--update-refs ignores non-branch decorations' ' + test_when_finished "git branch -D update-refs" && + test_when_finished "git checkout primary" && + git checkout -B update-refs no-conflict-branch && + ( + set_cat_todo_editor && + + # rebase.instructionFormat=%d loads normal log decorations before + # --update-refs adds its branch placeholders so we must ignore + # all non-local decorations. + test_must_fail git -c rebase.instructionFormat="%s%d" \ + rebase -i --update-refs HEAD^ >todo + ) && + grep ^update-ref todo >actual && + test_write_lines "update-ref refs/heads/no-conflict-branch" >expect && + test_cmp expect actual +' + test_expect_success '--update-refs updates refs correctly' ' git checkout -B update-refs no-conflict-branch && git branch -f base HEAD~4 && From 7c78d24c52d65f4442e521c575ef887481a38439 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:45 +0200 Subject: [PATCH 17/22] name-rev: wrap both blocks in braces See `CodingGuidelines`: - When there are multiple arms to a conditional and some of them require braces, enclose even a single line block in braces for consistency. [...] Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- builtin/name-rev.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 6188cf98ce0157..171e7bd0e98a46 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -466,9 +466,9 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf) if (!n) return NULL; - if (!n->generation) + if (!n->generation) { return n->tip_name; - else { + } else { strbuf_reset(buf); strbuf_addstr(buf, n->tip_name); strbuf_strip_suffix(buf, "^0"); @@ -516,9 +516,9 @@ static void name_rev_line(char *p, struct name_ref_data *data) for (p_start = p; *p; p++) { #define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f')) - if (!ishex(*p)) + if (!ishex(*p)) { counter = 0; - else if (++counter == hexsz && + } else if (++counter == hexsz && !ishex(*(p+1))) { struct object_id oid; const char *name = NULL; From b9c1be43eb30084570781569920a92799afdceaf Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:46 +0200 Subject: [PATCH 18/22] name-rev: run clang-format before factoring code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are about to move code around to prepare for adding a new command. Let’s deal with clang-format changes first in the affected areas. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- builtin/name-rev.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 171e7bd0e98a46..6357eaa76d0234 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -519,22 +519,22 @@ static void name_rev_line(char *p, struct name_ref_data *data) if (!ishex(*p)) { counter = 0; } else if (++counter == hexsz && - !ishex(*(p+1))) { + !ishex(*(p + 1))) { struct object_id oid; const char *name = NULL; - char c = *(p+1); + char c = *(p + 1); int p_len = p - p_start + 1; counter = 0; - *(p+1) = 0; + *(p + 1) = 0; if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) { struct object *o = lookup_object(the_repository, &oid); if (o) name = get_rev_name(o, &buf); } - *(p+1) = c; + *(p + 1) = c; if (!name) continue; @@ -571,9 +571,9 @@ int cmd_name_rev(int argc, OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")), OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")), OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"), - N_("only use refs matching ")), + N_("only use refs matching ")), OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"), - N_("ignore refs matching ")), + N_("ignore refs matching ")), OPT_GROUP(""), OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")), #ifndef WITH_BREAKING_CHANGES @@ -585,10 +585,10 @@ int cmd_name_rev(int argc, #endif /* WITH_BREAKING_CHANGES */ OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")), OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")), - OPT_BOOL(0, "always", &always, - N_("show abbreviated commit object as fallback")), + OPT_BOOL(0, "always", &always, + N_("show abbreviated commit object as fallback")), OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag, - N_("dereference tags in the input (internal use)")), + N_("dereference tags in the input (internal use)")), OPT_END(), }; From e2916329dbea62d033091a625340d3d79b686dc6 Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:47 +0200 Subject: [PATCH 19/22] name-rev: factor code for sharing with a new command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are about to introduce a new command git-format-rev(1) to this file. Let’s factor some code so that we can share it with the new command. We want to be able to format commits found in freeform text, and git-name-rev(1) already has a function for that but for symbolic names. Let’s use a tagged union for the command-specific payload. No functional changes. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- builtin/name-rev.c | 53 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 6357eaa76d0234..475efb0b82b752 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -272,6 +272,24 @@ struct name_ref_data { struct string_list exclude_filters; }; +enum command_type { + NAME_REV = 1, +}; + +struct command { + enum command_type type; + union { + int name_only; + } u; +}; + +static void init_name_rev_command(struct command *cmd, + int name_only) +{ + cmd->type = NAME_REV; + cmd->u.name_only = name_only; +} + static struct tip_table { struct tip_table_entry { struct object_id oid; @@ -507,7 +525,7 @@ static char const * const name_rev_usage[] = { NULL }; -static void name_rev_line(char *p, struct name_ref_data *data) +static void name_rev_line(char *p, struct command *cmd) { struct strbuf buf = STRBUF_INIT; int counter = 0; @@ -524,25 +542,32 @@ static void name_rev_line(char *p, struct name_ref_data *data) const char *name = NULL; char c = *(p + 1); int p_len = p - p_start + 1; + struct object *o = NULL; + int oid_ret = 1; counter = 0; *(p + 1) = 0; - if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) { - struct object *o = - lookup_object(the_repository, &oid); + oid_ret = repo_get_oid(the_repository, p - (hexsz - 1), &oid); + *(p + 1) = c; + + switch (cmd->type) { + case NAME_REV: + if (!oid_ret) + o = lookup_object(the_repository, &oid); if (o) name = get_rev_name(o, &buf); + if (!name) + continue; + if (cmd->u.name_only) + printf("%.*s%s", p_len - hexsz, p_start, name); + else + printf("%.*s (%s)", p_len, p_start, name); + break; + default: + BUG("uncovered case: %d", cmd->type); } - *(p + 1) = c; - - if (!name) - continue; - if (data->name_only) - printf("%.*s%s", p_len - hexsz, p_start, name); - else - printf("%.*s (%s)", p_len, p_start, name); p_start = p + 1; } } @@ -567,6 +592,7 @@ int cmd_name_rev(int argc, #endif int all = 0, annotate_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0; struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP }; + struct command cmd; struct option opts[] = { OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")), OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")), @@ -596,6 +622,7 @@ int cmd_name_rev(int argc, init_commit_rev_name(&rev_names); repo_config(the_repository, git_default_config, NULL); argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0); + init_name_rev_command(&cmd, data.name_only); #ifndef WITH_BREAKING_CHANGES if (transform_stdin) { @@ -663,7 +690,7 @@ int cmd_name_rev(int argc, while (strbuf_getline(&sb, stdin) != EOF) { strbuf_addch(&sb, '\n'); - name_rev_line(sb.buf, &data); + name_rev_line(sb.buf, &cmd); } strbuf_release(&sb); } else if (all) { From ae34adcf035b0b9bed705727e8884082ff4ae7aa Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:48 +0200 Subject: [PATCH 20/22] name-rev: make dedicated --annotate-stdin --name-only test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit split the `--name-only` handling: 1. `--annotate-stdin`: uses the new `struct command` 2. The rest: uses `struct name_ref_data` But there is no dedicated test for the option combination in (1). That means that the following tests will fail if you neglect to set `command.u.name_only` properly: name-rev --annotate-stdin works with commitGraph name-rev --annotate-stdin works with non-monotonic timestamps even though it has nothing to do with what these tests are supposed to test. Let’s add another regression test now that it is relevant. Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- t/t6120-describe.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 2c70cc561ad5f6..62789f763819e6 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -298,6 +298,20 @@ test_expect_success 'name-rev --annotate-stdin' ' test_cmp expect actual ' +test_expect_success 'name-rev --annotate-stdin --name-only' ' + >expect.unsorted && + for rev in $(git rev-list --all) + do + name=$(git name-rev --name-only $rev) && + echo "$name" >>expect.unsorted || return 1 + done && + sort expect && + git name-rev --annotate-stdin --name-only \ + actual.unsorted && + sort actual && + test_cmp expect actual +' + test_expect_success 'name-rev --stdin deprecated' ' git rev-list --all >list && if ! test_have_prereq WITH_BREAKING_CHANGES From 19e3106c4510bb50c370241c06e93f050f223d5c Mon Sep 17 00:00:00 2001 From: Kristoffer Haugsbakk Date: Mon, 11 May 2026 17:45:49 +0200 Subject: [PATCH 21/22] format-rev: introduce builtin for on-demand pretty formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new builtin for pretty formatting one revision expression per line or commit object names found in running text. Sometimes you want to format commits. Most of the time you’re walking the graph, e.g. getting a range of commits like `master..topic`. That’s a job for git-log(1). But there are times when you want to format commits that you encounter on demand: • Full hashes in running text that you might want to pretty-print • git-last-modified(1) outputs full hashes that you can do the same with • git-cherry(1) has `-v` for commit subject, but maybe you want something else? But now you can’t use git-log(1), git-show(1), or git-rev-list(1): • You can’t feed commits piecemeal to these commands, one input for one output; they block until standard in is closed • You can’t feed a list of possibly duplicate commits, like the output of git-last-modified(1); they effectively deduplicate the output Beyond these two points there’s also the input massage problem: you cannot feed mixed input (revisions mixed with arbitrary text). One might hope that git-cat-file(1) can save us. But it doesn’t support pretty formats. But there is one command that already both handles revisions as arguments, revisions on standard input, and even revisions mixed in with arbitrary text. Namely git-name-rev(1): the command for outputting symbolic names for commits. We made some room in `builtin/name-rev.c` two commits ago. Let’s now add this new git-format-rev(1) command. Taking inspiration from git-name-rev(1), there are two modes: • revs: like git-name-rev(1) in argv mode, but one revision per line on standard in • text: like git-name-rev(1) with `--annotate-stdin` *** We need to add this command to the exception list in `t/t1517-outside-repo.sh` because it uses “EXPERIMENTAL!” in the usage line. Helped-by: Phillip Wood Helped-by: Ramsay Jones Helped-by: Junio C Hamano Signed-off-by: Kristoffer Haugsbakk Signed-off-by: Junio C Hamano --- .gitignore | 1 + Documentation/git-format-rev.adoc | 215 ++++++++++++++++++++++++++++ Documentation/meson.build | 1 + Makefile | 1 + builtin.h | 1 + builtin/name-rev.c | 223 ++++++++++++++++++++++++++++++ command-list.txt | 1 + git.c | 1 + t/t1517-outside-repo.sh | 3 +- t/t6120-describe.sh | 194 ++++++++++++++++++++++++++ 10 files changed, 640 insertions(+), 1 deletion(-) create mode 100644 Documentation/git-format-rev.adoc diff --git a/.gitignore b/.gitignore index 78a45cb5bec991..091600cb884e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ /git-for-each-ref /git-for-each-repo /git-format-patch +/git-format-rev /git-fsck /git-fsck-objects /git-fsmonitor--daemon diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc new file mode 100644 index 00000000000000..c40d52e9f6d108 --- /dev/null +++ b/Documentation/git-format-rev.adoc @@ -0,0 +1,215 @@ +git-format-rev(1) +================= + +NAME +---- +git-format-rev - EXPERIMENTAL: Pretty format revisions on demand + + +SYNOPSIS +-------- +[synopsis] +(EXPERIMENTAL!) git format-rev --stdin-mode= --format= [--[no-]notes=] [-z] [--[no-]null-output] [--[no-]null-input] + +DESCRIPTION +----------- + +Pretty format revisions from standard input. + +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE. + +OPTIONS +------- + +`--stdin-mode=`:: + How to interpret standard input data: ++ +-- +`revs`;; Each line or record (see the <> + section) is interpreted as a commit. Any kind of revision + expression can be used (see linkgit:gitrevisions[7]). Annotated + tags are peeled (see linkgit:gitglossary[7]). ++ +The argument `rev` is also accepted. + +`text`;; Formats all commit object names found in freeform text. These + must the full object names, i.e. abbreviated hexidecimal object + names will not be interpreted. ++ +Anything that is parsed as an object name but that is not found to be a +commit object name is left alone (echoed). +-- + +`--format=`:: + Pretty format string. + +`--notes=`:: +`--no-notes`:: + Custom notes ref. Notes are displayed when using the `%N` + atom. See linkgit:git-notes[1]. + +`-z`:: +`--null`:: + Use _NUL_ character to terminate both input and output instead + of newline. This option cannot be negated. ++ +This is useful if both the input and output could contain newlines or if +the input to this command also uses _NUL_ character termination; see the +<> section below. ++ +The mode `--stdin-mode=text` can have use for this option when it needs +to process input like for example `git last-modified -z`; see the +<> section below. + +`--null-output`:: +`--no-null-output`:: + Use _NUL_ character to terminate output instead of newline. The + default is `--no-null-output`. ++ +This is useful if the output could contain newlines, for example if the +`%n` (newline) atom is used. + +`--null-input`:: +`--no-null-input`:: + Use _NUL_ character to terminate input instead of newline. The + default is `--no-null-input`. ++ +This is useful if the input revision expressions could contain newlines. + +[[io]] +INPUT AND OUTPUT FORMAT +----------------------- + +The command uses newlines for both input and output termination by +default. See the `-z`, `--null-output`, and `--null-input` options for +using _NUL_ character as the terminator. + +The mode `--stdin-mode=revs` outputs one formatted commit followed by +the terminator. This could either be called a _line_ or a _record_ in +case "line" is too suggestive of newline termination. + +Note that this means that the terminator character (newline or _NUL_) +acts as a _terminator_, not a _separator_. In other words, the final +line or record is also terminated by the terminator character. + +The mode `--stdin-mode=text` replaces each object name with the +formatted commit, i.e. the format `%s` would transform some commit +object name to `` without any termination. Like this: + +---- +Did we not fix this in ""? +---- + +It is safe to interactively read and write from this command since each +record is immediately flushed. + +[[examples]] +EXAMPLES +-------- + +The command linkgit:git-last-modified[1] shows the commit that each file +was last modified in. + +---- +$ git last-modified -- README.md Makefile +7798034171030be0909c56377a4e0e10e6d2df93 Makefile +c50fbb2dd225e7e82abba4380423ae105089f4d7 README.md +---- + +We can pipe the result to this command in order to replace the object +name with the commit author. + +---- +$ git last-modified -- README.md Makefile | + git format-rev --stdin-mode=text --format=%an +Junio C Hamano Makefile +Todd Zullinger README.md +---- + +Another example is _formatting commits in commit messages_. Given this commit message: + +---- +Fix off-by-one error + +Fix off-by-one error introduced in +e83c5163316f89bfbde7d9ab23ca2e25604af290. + +We thought we fixed this in 5569bf9bbedd63a00780fc5c110e0cfab3aa97b9 but +that only covered 1/3 of the faulty cases. +---- + +We can format the commits and use par(1) to reflow the text, say in a +`commit-msg` hook: + +---- +$ git config set hook.reference-commits.event commit-msg +$ git config set hook.reference-commits.command reference-commits +$ cat $(which reference-commits) +#/bin/sh + +msg="$1" +rewritten=$(mktemp) +git format-rev --stdin-mode=text --format=reference <"$msg" | + par >"$rewritten" +mv "$rewritten" "$msg" +---- + +Which will produce something like this: + +---- +Fix off-by-one error + +Fix off-by-one error introduced in e83c5163316 (Implement better memory +allocator, 2005-04-07). + +We thought we fixed this in 5569bf9bbed (Fix memory allocator, +2005-06-22) but that only covered 1/3 of the faulty cases. +---- + +DISCUSSION +---------- + +This command lets you format any number of revisions in any order +through one command invocation. Consider the +linkgit:git-last-modified[1] case from the <> section +above: + +1. There might be hundreds of files +2. Commits can be repeated, i.e. two or more files were last modified in + the same commit + +Two widely-used commands which pretty formats commits are +linkgit:git-log[1] and linkgit:git-show[1]. It turns out that they are +not a good fit for the above use case. + +- The output of linkgit:git-last-modified[1] would have to be processed + in stages since you need to transform the first column separately and + then link the author to the filename. But this is surmountable. +- You can feed each commit to `git show` or `git log --no-walk -1`. But + that means that you need to create a process for each line. +- Let’s say that you want to use one process, not one per line. So you + want to feed all the commits to the command. Now you face the problem + that you have to feed all the commits to the commands before you get + any output (this is also the case for the `--stdin` modes). In other + words, you cannot loop through each line, get the author for the + commit, and output the author and the filename. You need to feed all + the commits, get back all the output, and match the output with the + filename. +- But the next problem is that commands will deduplicate the input and + only output one commit one single time only. Thus you cannot make the + output order match the input order, since a commit could have been + repeated in the original input. + +In short, it is straightforward to use these two commands if you use one +process per line. It is much more work if you just want to use one +process, but still doable. In contrast, this problem is solved with just +another shell pipeline with this command. + +SEE ALSO +-------- +linkgit:git-name-rev[1], +linkgit:git-log[1]. + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/meson.build b/Documentation/meson.build index f02dbc20cbcb86..3de4333b836b8d 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -55,6 +55,7 @@ manpages = { 'git-for-each-ref.adoc' : 1, 'git-for-each-repo.adoc' : 1, 'git-format-patch.adoc' : 1, + 'git-format-rev.adoc' : 1, 'git-fsck-objects.adoc' : 1, 'git-fsck.adoc' : 1, 'git-fsmonitor--daemon.adoc' : 1, diff --git a/Makefile b/Makefile index 8aa489f3b6812f..f02f008a51e32b 100644 --- a/Makefile +++ b/Makefile @@ -892,6 +892,7 @@ BUILT_INS += $(patsubst builtin/%.o,git-%$X,$(BUILTIN_OBJS)) BUILT_INS += git-cherry$X BUILT_INS += git-cherry-pick$X BUILT_INS += git-format-patch$X +BUILT_INS += git-format-rev$X BUILT_INS += git-fsck-objects$X BUILT_INS += git-init$X BUILT_INS += git-maintenance$X diff --git a/builtin.h b/builtin.h index e5e16ecaa6c9d7..d640a1c1e61114 100644 --- a/builtin.h +++ b/builtin.h @@ -189,6 +189,7 @@ int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, struct re int cmd_for_each_ref(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_for_each_repo(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_format_patch(int argc, const char **argv, const char *prefix, struct repository *repo); +int cmd_format_rev(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_fsck(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_gc(int argc, const char **argv, const char *prefix, struct repository *repo); diff --git a/builtin/name-rev.c b/builtin/name-rev.c index 475efb0b82b752..5494b0424b3616 100644 --- a/builtin/name-rev.c +++ b/builtin/name-rev.c @@ -18,6 +18,10 @@ #include "commit-graph.h" #include "wildmatch.h" #include "mem-pool.h" +#include "pretty.h" +#include "revision.h" +#include "notes.h" +#include "write-or-die.h" /* * One day. See the 'name a rev shortly after epoch' test in t6120 when @@ -272,14 +276,26 @@ struct name_ref_data { struct string_list exclude_filters; }; +struct pretty_format { + struct pretty_print_context ctx; + struct userformat_want want; +}; + enum command_type { NAME_REV = 1, + FORMAT_REV = 2, +}; + +enum stdin_mode { + TEXT = 1, + REVS = 2, }; struct command { enum command_type type; union { int name_only; + struct pretty_format *pretty_format; } u; }; @@ -290,6 +306,13 @@ static void init_name_rev_command(struct command *cmd, cmd->u.name_only = name_only; } +static void init_format_rev_command(struct command *cmd, + struct pretty_format *pretty_format) +{ + cmd->type = FORMAT_REV; + cmd->u.pretty_format = pretty_format; +} + static struct tip_table { struct tip_table_entry { struct object_id oid; @@ -495,6 +518,27 @@ static const char *get_rev_name(const struct object *o, struct strbuf *buf) } } +static const char *get_format_rev(const struct commit *c, + struct pretty_format *format_ctx, + struct strbuf *buf) +{ + strbuf_reset(buf); + + if (format_ctx->want.notes) { + struct strbuf notebuf = STRBUF_INIT; + + format_display_notes(&c->object.oid, ¬ebuf, + get_log_output_encoding(), + format_ctx->ctx.fmt == CMIT_FMT_USERFORMAT); + format_ctx->ctx.notes_message = strbuf_detach(¬ebuf, NULL); + } + + pretty_print_commit(&format_ctx->ctx, c, buf); + FREE_AND_NULL(format_ctx->ctx.notes_message); + + return buf->buf; +} + static void show_name(const struct object *obj, const char *caller_name, int always, int allow_undefined, int name_only) @@ -564,6 +608,18 @@ static void name_rev_line(char *p, struct command *cmd) else printf("%.*s (%s)", p_len, p_start, name); break; + case FORMAT_REV: + if (!oid_ret) + o = parse_object(the_repository, &oid); + if (o && o->type == OBJ_COMMIT) + name = get_format_rev((const struct commit *)o, + cmd->u.pretty_format, + &buf); + if (name) + printf("%.*s%s", p_len - hexsz, p_start, name); + else + printf("%.*s", p_len, p_start); + break; default: BUG("uncovered case: %d", cmd->type); } @@ -717,3 +773,170 @@ int cmd_name_rev(int argc, object_array_clear(&revs); return 0; } + +struct format_nul_data { + bool nul_input; + bool nul_output; +}; + +static int format_nul_cb(const struct option *option, + const char *arg, + int unset) +{ + struct format_nul_data *data = option->value; + data->nul_input = 1; + data->nul_output = 1; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + return 0; +} + +static enum stdin_mode parse_stdin_mode(const char *stdin_mode) +{ + if (!strcmp(stdin_mode, "text")) + return TEXT; + else if (!strcmp(stdin_mode, "revs") || + !strcmp(stdin_mode, "rev")) + return REVS; + else + die(_("'%s' needs to be either text, revs, or rev"), + "--stdin-mode"); +} + +static char const *const format_rev_usage[] = { + N_("(EXPERIMENTAL!) git format-rev --stdin-mode= " + "--format= [--[no-]notes=] " + "[-z] [--[no-]null-output] [--[no-]null-input]"), + NULL +}; + +int cmd_format_rev(int argc, + const char **argv, + const char *prefix, + struct repository *repo UNUSED) +{ + const char *format = NULL; + enum stdin_mode stdin_mode; + const char *stdin_mode_arg = NULL; + struct format_nul_data nul_data = { 0, 0 }; + char output_terminator; + strbuf_getline_fn getline_fn; + struct display_notes_opt format_notes_opt; + struct rev_info format_rev = REV_INFO_INIT; + struct pretty_format format_pp = { 0 }; + struct string_list notes = STRING_LIST_INIT_NODUP; + struct strbuf scratch_buf = STRBUF_INIT; + struct command cmd; + struct option opts[] = { + OPT_STRING(0, "format", &format, N_("format"), + N_("pretty format to use")), + OPT_STRING(0, "stdin-mode", &stdin_mode_arg, N_("stdin-mode"), + N_("how revs are processed")), + OPT_STRING_LIST(0, "notes", ¬es, N_("notes"), + N_("display notes for pretty format")), + OPT_CALLBACK_F('z', "null", &nul_data, N_("z"), + N_("Use NUL for input and output termination"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, format_nul_cb), + OPT_BOOL(0, "null-input", &nul_data.nul_input, + N_("Use NUL for input termination")), + OPT_BOOL(0, "null-output", &nul_data.nul_output, + N_("Use NUL for output termination")), + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, opts, format_rev_usage, 0); + + if (argc > 0) { + error(_("too many arguments")); + usage_with_options(format_rev_usage, opts); + } + + if (!format) + die(_("'%s' is required"), "--format"); + if (!stdin_mode_arg) + die(_("'%s' is required"), "--stdin-mode"); + + getline_fn = nul_data.nul_input ? strbuf_getline_nul : strbuf_getline_lf; + output_terminator = nul_data.nul_output ? '\0' : '\n'; + + init_display_notes(&format_notes_opt); + stdin_mode = parse_stdin_mode(stdin_mode_arg); + + get_commit_format(format, &format_rev); + format_pp.ctx.rev = &format_rev; + format_pp.ctx.fmt = format_rev.commit_format; + format_pp.ctx.abbrev = format_rev.abbrev; + format_pp.ctx.date_mode_explicit = format_rev.date_mode_explicit; + format_pp.ctx.date_mode = format_rev.date_mode; + format_pp.ctx.color = GIT_COLOR_AUTO; + + userformat_find_requirements(format, + &format_pp.want); + if (format_pp.want.notes) { + int ignore_show_notes = 0; + struct string_list_item *n; + + for_each_string_list_item(n, ¬es) + enable_ref_display_notes(&format_notes_opt, + &ignore_show_notes, + n->string); + load_display_notes(&format_notes_opt); + } + + init_format_rev_command(&cmd, &format_pp); + + switch (stdin_mode) { + case TEXT: + while (getline_fn(&scratch_buf, stdin) != EOF) { + name_rev_line(scratch_buf.buf, &cmd); + /* + * We do not pass on the terminator to name_rev_line, + * unlike name-rev. + */ + printf("%c", output_terminator); + maybe_flush_or_die(stdout, "stdout"); + } + break; + case REVS: + while (getline_fn(&scratch_buf, stdin) != EOF) { + struct object_id oid; + struct object *object; + struct object *peeled; + + if (repo_get_oid(the_repository, scratch_buf.buf, &oid)) { + fprintf(stderr, "Could not get object name for %s. Skipping.\n", + scratch_buf.buf); + continue; + } + + object = parse_object(the_repository, &oid); + if (!object) { + fprintf(stderr, "Could not get object for %s. Skipping.\n", + scratch_buf.buf); + continue; + } + + peeled = deref_tag(the_repository, object, scratch_buf.buf, 0); + if (!peeled || peeled->type != OBJ_COMMIT) { + fprintf(stderr, + "Could not get commit for %s. Skipping.\n", + scratch_buf.buf); + continue; + } + + get_format_rev((struct commit *)peeled, + &format_pp, &scratch_buf); + printf("%s%c", scratch_buf.buf, output_terminator); + maybe_flush_or_die(stdout, "stdout"); + strbuf_release(&scratch_buf); + } + break; + default: + BUG("uncovered case: %d", stdin_mode); + } + + strbuf_release(&scratch_buf); + string_list_clear(¬es, 0); + release_display_notes(&format_notes_opt); + return 0; +} diff --git a/command-list.txt b/command-list.txt index accd3d0c4b5524..696484b92bc43d 100644 --- a/command-list.txt +++ b/command-list.txt @@ -108,6 +108,7 @@ git-fmt-merge-msg purehelpers git-for-each-ref plumbinginterrogators git-for-each-repo plumbinginterrogators git-format-patch mainporcelain +git-format-rev plumbinginterrogators git-fsck ancillaryinterrogators complete git-gc mainporcelain git-get-tar-commit-id plumbinginterrogators diff --git a/git.c b/git.c index c5fad56813f437..68c6401698faed 100644 --- a/git.c +++ b/git.c @@ -578,6 +578,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 }, + { "format-rev", cmd_format_rev, RUN_SETUP }, { "fsck", cmd_fsck, RUN_SETUP }, { "fsck-objects", cmd_fsck, RUN_SETUP }, { "fsmonitor--daemon", cmd_fsmonitor__daemon, RUN_SETUP }, diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index c824c1a25cf27e..360a93233433e9 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -114,7 +114,8 @@ do archimport | citool | credential-netrc | credential-libsecret | \ credential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \ daemon | \ - difftool--helper | filter-branch | fsck-objects | get-tar-commit-id | \ + difftool--helper | filter-branch | format-rev | fsck-objects | \ + get-tar-commit-id | \ gui | gui--askpass | \ http-backend | http-fetch | http-push | init-db | \ merge-octopus | merge-one-file | merge-resolve | mergetool | \ diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 62789f763819e6..8ee3d2c37d0253 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -801,4 +801,198 @@ test_expect_success 'do not be fooled by invalid describe format ' ' test_must_fail git cat-file -t "refs/tags/super-invalid/./../...../ ~^:/?*[////\\\\\\&}/busted.lock-42-g"$(cat out) ' +test_expect_success 'setup: format-rev' ' + mkdir repo-format && + git -C repo-format init && + test_commit -C repo-format first && + test_commit -C repo-format second && + test_commit -C repo-format third && + test_commit -C repo-format fourth && + test_commit -C repo-format fifth && + test_commit -C repo-format sixth && + test_commit -C repo-format seventh && + test_commit -C repo-format eighth +' + +test_expect_success 'format-rev --stdin-mode=revs' ' + cat >expect <<-\EOF && + eighth + seventh + fifth + EOF + git -C repo-format format-rev --stdin-mode=revs \ + --format=%s >actual <<-\EOF && + HEAD + HEAD~ + HEAD~3 + EOF + test_cmp expect actual +' + +test_expect_success 'format-rev --stdin-mode=text from rev-list same as log' ' + git -C repo-format log --format=reference >expect && + test_file_not_empty expect && + git -C repo-format rev-list HEAD >list && + git -C repo-format format-rev --stdin-mode=text \ + --format=reference actual && + test_cmp expect actual +' + +test_expect_success 'format-rev --stdin-mode=text with running text and tree oid' ' + cmit_oid=$(git -C repo-format rev-parse fifth) && + reference=$(git -C repo-format log -n1 --format=reference fifth) && + tree=$(git -C repo-format rev-parse HEAD^{tree}) && + cat >expect <<-EOF && + We thought we fixed this in ${reference}. + But look at this tree: ${tree}. + EOF + git -C repo-format format-rev --stdin-mode=text --format=reference \ + >actual <<-EOF && + We thought we fixed this in ${cmit_oid}. + But look at this tree: ${tree}. + EOF + test_cmp expect actual +' + +test_expect_success 'format-rev with %N (note)' ' + test_when_finished "git -C repo-format notes remove" && + git -C repo-format notes add -m"Make a note" && + printf "Make a note\n\n\n" >expect && + git -C repo-format format-rev --stdin-mode=revs \ + --format="tformat:%N" \ + >actual <<-\EOF && + HEAD + HEAD~ + EOF + test_cmp expect actual +' + +test_expect_success 'format-rev --notes (custom notes ref)' ' + # One custom notes ref + test_when_finished "git -C repo-format notes remove" && + test_when_finished "git -C repo-format notes --ref=word remove" && + git -C repo-format notes add -m"default" && + git -C repo-format notes --ref=word add -m"custom" && + printf "custom\n\n" >expect && + git -C repo-format format-rev --stdin-mode=revs \ + --format="tformat:%N" \ + --notes=word \ + >actual <<-\EOF && + HEAD + EOF + test_cmp expect actual && + # Glob all + printf "default\ncustom\n\n" >expect && + git -C repo-format format-rev --stdin-mode=revs \ + --format="tformat:%N" \ + --notes=* >actual <<-\EOF && + HEAD + EOF + test_cmp expect actual +' + +test_expect_success 'format-rev --stdin-mode=revs on annotated tag peels to commit' ' + test_when_finished "git -C repo-format tag -d version" && + git -C repo-format tag -a -m"new version" version && + cat >expect <<-\EOF && + eighth + EOF + git -C repo-format format-rev --stdin-mode=revs \ + --format=%s \ + >actual <<-\EOF && + version + EOF + test_cmp expect actual +' + +test_expect_success 'format-rev --stdin-mode=revs lookup failures' ' + test_when_finished "git -C repo-format tag -d tag-to-tree" && + invalid_syntax=not-valid && + non_existing_oid=${EMPTY_BLOB} && + tree=$(git -C repo-format rev-parse eighth^{tree}) && + git -C repo-format tag -a -mmessage tag-to-tree "$tree" && + tag_to_tree=$(git -C repo-format rev-parse tag-to-tree) && + cat >expect <<-EOF && + Could not get object name for ${invalid_syntax}. Skipping. + Could not get object for ${non_existing_oid}. Skipping. + Could not get commit for ${tree}. Skipping. + Could not get commit for ${tag_to_tree}. Skipping. + EOF + git -C repo-format format-rev --stdin-mode=revs \ + --format=%s \ + 2>actual >out <<-EOF && + ${invalid_syntax} + ${non_existing_oid} + ${tree} + ${tag_to_tree} + EOF + test_line_count = 0 out && + test_cmp expect actual +' + + +test_expect_success 'format-rev -z --stdin-mode=text with object name lookup failures' ' + printf "%s\0" "$(git -C repo-format rev-parse HEAD)" >input && + printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>input && + printf "%s\0" "$EMPTY_BLOB" >>input && + printf "%s\0" "$(git -C repo-format log --format=%s -1)" >expect && + printf "%s\0" "$(git -C repo-format rev-parse HEAD^{tree})" >>expect && + printf "%s\0" "$EMPTY_BLOB" >>expect && + git -C repo-format format-rev --stdin-mode=text \ + --format=%s -z actual && + test_cmp expect actual +' + +test_expect_success 'setup: format-rev input and output separators' ' + git -C repo-format rev-list HEAD >input-lf && + git -C repo-format rev-list -z HEAD >input-nul && + git -C repo-format log --format=%s >output-lf && + git -C repo-format log -z --format=%s >output-nul && + echo revs >stdin-modes && + echo text >>stdin-modes +' + +while read mode +do + test_expect_success "format-rev -z --stdin-mode=$mode" ' + cat output-nul >expect && + git -C repo-format format-rev --stdin-mode="$mode" \ + --format=%s -z actual && + test_cmp expect actual + ' + + test_expect_success "format-rev -z --no-null-input --no-null-output --stdin-mode=$mode" ' + cat output-lf >expect && + git -C repo-format format-rev --stdin-mode="$mode" \ + --format=%s -z --no-null-input --no-null-output \ + actual && + test_cmp expect actual + ' + + test_expect_success "format-rev ---null-input --stdin-mode=$mode" ' + cat output-lf >expect && + git -C repo-format format-rev --stdin-mode="$mode" \ + --format=%s --null-input \ + actual && + test_cmp expect actual + ' + + test_expect_success "format-rev --null-output --stdin-mode=$mode" ' + cat output-nul >expect && + git -C repo-format format-rev --stdin-mode="$mode" \ + --format=%s --null-output \ + actual && + test_cmp expect actual + ' + + test_expect_success "format-rev -z --stdin-mode=$mode with multi-line output" ' + format="%s%n%aI" && + git -C repo-format log -z --format="$format" \ + >expect && + git -C repo-format format-rev --stdin-mode="$mode" \ + --format="$format" -z actual && + test_cmp expect actual + ' +done Date: Mon, 18 May 2026 21:57:40 +0900 Subject: [PATCH 22/22] The 4th batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.55.0.adoc | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Documentation/RelNotes/2.55.0.adoc b/Documentation/RelNotes/2.55.0.adoc index 4f254547771505..8cf939d53638a6 100644 --- a/Documentation/RelNotes/2.55.0.adoc +++ b/Documentation/RelNotes/2.55.0.adoc @@ -21,6 +21,10 @@ UI, Workflows & Features branch, but it gave only one chance to resolve conflicts. The command was taught to create a stash to save the local changes. + * A new builtin "git format-rev" is introduced for pretty formatting + one revision expression per line or commit object names found in + running text. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -31,6 +35,8 @@ Performance, Internal Implementation, Development Support etc. * Rust support is enabled by default (but still allows opting out) in some future version of Git. + * Preparation of the xdiff/ codebase to work with Rust. + Fixes since v2.54 ----------------- @@ -76,6 +82,30 @@ Fixes since v2.54 disabling C11 language features, causing build failures.. (merge 0a6d29090c ps/clang-w-glibc-2.43-and-_Generic later to maint). + * The 'http.emptyAuth=auto' configuration now correctly attempts + Negotiate authentication before falling back to manual credentials. + This allows seamless Kerberos ticket-based authentication without + requiring users to explicitly set 'http.emptyAuth=true'. + (merge 4919938d28 mc/http-emptyauth-negotiate-fix later to maint). + + * Ramifications of turning off commit-graph has been documented a bit + more clearly. + (merge 48c855bb8f kh/doc-commit-graph later to maint). + + * "git rebase --update-refs", when used with an rebase.instructionFormat + with "%d" (describe) in it, tried to update local branch HEAD by + mistake, which has been corrected. + (merge 106b6885c7 ag/rebase-update-refs-limit-to-branches later to maint). + + * Tweak the way how sideband messages from remote are printed while + we talk with a remote repository to avoid tickling terminal + emulator glitches. + (merge 31e8fcabd8 rs/sideband-clear-line-before-print later to maint). + + * The configuration variable submodule.fetchJobs was not read correctly, + which has been corrected. + (merge aa45a5902f sj/submodule-update-clone-config-fix later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 80f4b802e9 ja/doc-difftool-synopsis-style later to maint). (merge b96490241e jc/doc-timestamps-in-stat later to maint). @@ -83,3 +113,5 @@ Fixes since v2.54 (merge ef85286e51 ss/t7004-unhide-git-failures later to maint). (merge 7584d10bc2 mf/format-patch-cover-letter-format-docfix later to maint). (merge 8547908eb3 pw/rename-to-get-current-worktree later to maint). + (merge 890229b3f3 sg/t6112-unwanted-tilde-expansion-fix later to maint). + (merge ab9753e7bc kh/doc-restore-double-underscores-fix later to maint).