* Re: [PATCH v3 0/9] sequencer: do not record dropped commits as rewritten
From: Junio C Hamano @ 2026-07-19 19:29 UTC (permalink / raw)
To: Phillip Wood
Cc: git, Uwe Kleine-König, Oswald Buddenhagen, Farid Zakaria,
Andrei Rybak
In-Reply-To: <cover.1784128921.git.phillip.wood@dunelm.org.uk>
Phillip Wood <phillip.wood123@gmail.com> writes:
> Thanks to everyone who commented on v2. I've dropped patch 2 which
> Andrei pointed out was pointless and tried to make the remaining
> commit messages clearer as requested by Oswald.
>
> If a commit gets dropped because its changes are already upstream
> then we should not record it as rewritten. As well as confusing any
> post-rewrite hooks this means we end up copying the notes from the
> dropped commit to the commit that was picked immediately before the
> one that was dropped.
>
> This series is structured as follows:
>
> Patch 1 restores some test coverage that was lost when the default
> rebase backend was changed.
>
> Patches 2 & 3 fix the return value of do_pick_commit() when an external
> command fails (this is in preparation for patch 8).
>
> Patches 4-7 try and simplify the control flow in pick_one_commit()
> in preparation for patch 8.
>
> Patch 8 changes the return type of do_pick_commit() to an enum.
>
> Patch 9 adds a new member to the enum from patch 8 for commits that
> are dropped when they become empty and uses that to stop them from
> being recorded as rewritten.
I see Phillip Cc'ed everybody who participated in the review for the
previous iterations, which is very much appreciated.
It looks like this is now ready to go? Any further comments?
Thanks.
^ permalink raw reply
* Re: [PATCH v19 5/7] branch: add --delete-merged <branch>
From: Phillip Wood @ 2026-07-19 19:42 UTC (permalink / raw)
To: Junio C Hamano, Harald Nordgren
Cc: Harald Nordgren via GitGitGadget, git, Kristoffer Haugsbakk,
Johannes Sixt
In-Reply-To: <xmqqik6an5t3.fsf@gitster.g>
On 19/07/2026 20:22, Junio C Hamano wrote:
> Harald Nordgren <haraldnordgren@gmail.com> writes:
>
>> I think I can fix this with
>>
>> ```
>> -+ if (strset_contains(data->deletable, ref->name))
>> ++ if (strset_contains(data->deletable, ref->name) ||
>> ++ strset_contains(data->spared, ref->name))
>> ```
>>
>> I used your example to write a test about it as well.
>>
>>
>> Harald
>
> I do not claim that the single example I gave covers all the issues
> that arise from failing to analyze the dependency graph, or from
> attempting to solve the problem sequentially, which makes the
> solution depend on the order in which branches are visited.
>
> I have a suspicion that it may be unavoidable to employ a multi-pass
> approach that iteratively identifies all branches transitively
> needed by any surviving branch, though that is merely a hunch,
> unsupported by any proof.
I think it would certainly be easier to reason about if we did not
mutate data->deletable in spare_stacked_base() in removed the spared
branches with
strset_for_each_entry(&spared, &iter, entry)
strset_remove(&deletable, entry);
after the call to refs_for_each_branch_ref() returns in
spare_stacked_branches().
I'll try and have a proper look at this iteration in the comming week.
Thanks
Phillip
^ permalink raw reply
* [PATCH 0/4] A new batch of synopsis conversion
From: Jean-Noël Avila via GitGitGadget @ 2026-07-19 19:54 UTC (permalink / raw)
To: git; +Cc: Jean-Noël Avila
The conversion to synopsis continues. Now the commands are less common, so I
may propose larger PRs if it's ok.
Jean-Noël Avila (4):
doc: convert git-imap-send synopsis and options to new style
doc: convert git-format-patch synopsis and options to new style
doc: convert git-send-email synopsis and options to new style
doc: convert git-request-pull synopsis and options to new style
Documentation/config/imap.adoc | 2 +-
Documentation/config/sendemail.adoc | 92 +++++-----
Documentation/diff-options.adoc | 8 +-
Documentation/git-format-patch.adoc | 264 ++++++++++++++--------------
Documentation/git-imap-send.adoc | 7 +-
Documentation/git-request-pull.adoc | 38 ++--
Documentation/git-send-email.adoc | 162 ++++++++---------
7 files changed, 292 insertions(+), 281 deletions(-)
base-commit: 08621c32d5536babd139ab1a9086349b3672edd6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2185%2Fjnavila%2Fconvert_synopsis_202606-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2185/jnavila/convert_synopsis_202606-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2185
--
gitgitgadget
^ permalink raw reply
* [PATCH 1/4] doc: convert git-imap-send synopsis and options to new style
From: Jean-Noël Avila via GitGitGadget @ 2026-07-19 19:54 UTC (permalink / raw)
To: git; +Cc: Jean-Noël Avila, Jean-Noël Avila
In-Reply-To: <pull.2185.git.1784490878.gitgitgadget@gmail.com>
From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= <jn.avila@free.fr>
Convert git-imap-send from [verse]/single-quote style to the modern
synopsis-block style:
- Replace [verse] with [synopsis] in SYNOPSIS block
- Backtick-quote all OPTIONS terms
- Backtick-quote all config keys in config/imap.adoc
- Backtick-quote bare config key references in prose
Signed-off-by: Jean-Noël Avila <jn.avila@free.fr>
---
Documentation/config/imap.adoc | 2 +-
Documentation/git-imap-send.adoc | 7 +++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/Documentation/config/imap.adoc b/Documentation/config/imap.adoc
index cb8f5e2700..6b97776bc3 100644
--- a/Documentation/config/imap.adoc
+++ b/Documentation/config/imap.adoc
@@ -41,7 +41,7 @@
`imap.authMethod`::
Specify the authentication method for authenticating with the IMAP server.
If Git was built with the NO_CURL option, or if your curl version is older
- than 7.34.0, or if you're running git-imap-send with the `--no-curl`
+ than 7.34.0, or if you're running `git-imap-send` with the `--no-curl`
option, the only supported methods are `PLAIN`, `CRAM-MD5`, `OAUTHBEARER`
and `XOAUTH2`. If this is not set then `git imap-send` uses the basic IMAP
plaintext `LOGIN` command.
diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc
index 538b91afc0..dd1e0a3718 100644
--- a/Documentation/git-imap-send.adoc
+++ b/Documentation/git-imap-send.adoc
@@ -192,7 +192,10 @@ supports only `XOAUTH2` as the mechanism.
Once the commits are ready to be sent, run the following command:
- $ git format-patch --cover-letter -M --stdout origin/master | git imap-send
+
+---------
+$ git format-patch --cover-letter -M --stdout origin/master | git imap-send
+---------
Just make sure to disable line wrapping in the email client (Gmail's web
interface will wrap lines no matter what, so you need to use a real
@@ -217,7 +220,7 @@ users may wish to visit this web page for more information:
SEE ALSO
--------
-linkgit:git-format-patch[1], linkgit:git-send-email[1], mbox(5)
+linkgit:git-format-patch[1], linkgit:git-send-email[1], `mbox`(5)
GIT
---
--
gitgitgadget
^ permalink raw reply related
* [PATCH 2/4] doc: convert git-format-patch synopsis and options to new style
From: Jean-Noël Avila via GitGitGadget @ 2026-07-19 19:54 UTC (permalink / raw)
To: git; +Cc: Jean-Noël Avila, Jean-Noël Avila
In-Reply-To: <pull.2185.git.1784490878.gitgitgadget@gmail.com>
From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= <jn.avila@free.fr>
Replace [verse] with [synopsis] in the SYNOPSIS block and remove
single-quote formatting from the command name.
Backtick-quote all option terms in the OPTIONS section, convert
standalone placeholders to _<placeholder>_ form, and convert
single-quoted commands and tools in prose to backtick form.
Also update the included files:
- diff-options.adoc: backtick-quote the git-format-patch-specific
option terms (-p, --no-stat, --max-depth=<depth>)
- format-patch-caveats.adoc: convert patch(1) to `patch`(1)
Signed-off-by: Jean-Noël Avila <jn.avila@free.fr>
---
Documentation/diff-options.adoc | 8 +-
Documentation/git-format-patch.adoc | 264 ++++++++++++++--------------
2 files changed, 136 insertions(+), 136 deletions(-)
diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc
index c8242e2462..e605d28678 100644
--- a/Documentation/diff-options.adoc
+++ b/Documentation/diff-options.adoc
@@ -13,8 +13,8 @@ endif::git-diff[]
endif::git-format-patch[]
ifdef::git-format-patch[]
--p::
---no-stat::
+`-p`::
+`--no-stat`::
Generate plain patches without any diffstats.
endif::git-format-patch[]
@@ -893,8 +893,8 @@ endif::git-format-patch[]
reverted with `--ita-visible-in-index`. Both options are
experimental and could be removed in future.
---max-depth=<depth>::
- For each pathspec given on command line, descend at most `<depth>`
+`--max-depth=<depth>`::
+ For each pathspec given on command line, descend at most _<depth>_
levels of directories. A value of `-1` means no limit.
Cannot be combined with wildcards in the pathspec.
Given a tree containing `foo/bar/baz`, the following list shows the
diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc
index f7905c0f7c..5eccb2cf60 100644
--- a/Documentation/git-format-patch.adoc
+++ b/Documentation/git-format-patch.adoc
@@ -8,8 +8,8 @@ git-format-patch - Prepare patches for e-mail submission
SYNOPSIS
--------
-[verse]
-'git format-patch' [-k] [(-o|--output-directory) <dir> | --stdout]
+[synopsis]
+git format-patch [-k] [(-o|--output-directory) <dir> | --stdout]
[--no-thread | --thread[=<style>]]
[(--attach|--inline)[=<boundary>] | --no-attach]
[-s | --signoff]
@@ -40,20 +40,20 @@ DESCRIPTION
Prepare each non-merge commit with its "patch" in
one "message" per commit, formatted to resemble a UNIX mailbox.
The output of this command is convenient for e-mail submission or
-for use with 'git am'.
+for use with `git am`.
A "message" generated by the command consists of three parts:
* A brief metadata header that begins with `From <commit>`
with a fixed `Mon Sep 17 00:00:00 2001` datestamp to help programs
- like "file(1)" to recognize that the file is an output from this
+ like `file`(1) to recognize that the file is an output from this
command, fields that record the author identity, the author date,
and the title of the change (taken from the first paragraph of the
commit log message).
* The second and subsequent paragraphs of the commit log message.
-* The "patch", which is the "diff -p --stat" output (see
+* The "patch", which is the `diff -p --stat` output (see
linkgit:git-diff[1]) between the commit and its parent.
The log message and the patch are separated by a line with a
@@ -61,18 +61,18 @@ three-dash line.
There are two ways to specify which commits to operate on.
-1. A single commit, <since>, specifies that the commits leading
+1. A single commit, _<since>_, specifies that the commits leading
to the tip of the current branch that are not in the history
- that leads to the <since> to be output.
+ that leads to the _<since>_ to be output.
-2. Generic <revision-range> expression (see "SPECIFYING
- REVISIONS" section in linkgit:gitrevisions[7]) means the
+2. Generic _<revision-range>_ expression (see 'SPECIFYING
+ REVISIONS' section in linkgit:gitrevisions[7]) means the
commits in the specified range.
-The first rule takes precedence in the case of a single <commit>. To
+The first rule takes precedence in the case of a single _<commit>_. To
apply the second rule, i.e., format everything since the beginning of
-history up until <commit>, use the `--root` option: `git format-patch
---root <commit>`. If you want to format only <commit> itself, you
+history up until _<commit>_, use the `--root` option: `git format-patch
+--root <commit>`. If you want to format only _<commit>_ itself, you
can do this with `git format-patch -1 <commit>`.
By default, each output file is numbered sequentially from 1, and uses the
@@ -82,7 +82,7 @@ will only be numbers, without the first line of the commit appended.
The names of the output files are printed to standard
output, unless the `--stdout` option is specified.
-If `-o` is specified, output files are created in <dir>. Otherwise
+If `-o` is specified, output files are created in _<dir>_. Otherwise
they are created in the current working directory. The default path
can be set with the `format.outputDirectory` configuration option.
The `-o` option takes precedence over `format.outputDirectory`.
@@ -108,119 +108,119 @@ OPTIONS
:git-format-patch: 1
include::diff-options.adoc[]
--<n>::
- Prepare patches from the topmost <n> commits.
+`-<n>`::
+ Prepare patches from the topmost _<n>_ commits.
--o <dir>::
---output-directory <dir>::
- Use <dir> to store the resulting files, instead of the
+`-o <dir>`::
+`--output-directory=<dir>`::
+ Use _<dir>_ to store the resulting files, instead of the
current working directory.
--n::
---numbered::
+`-n`::
+`--numbered`::
Name output in '[PATCH n/m]' format, even with a single patch.
--N::
---no-numbered::
+`-N`::
+`--no-numbered`::
Name output in '[PATCH]' format.
---start-number <n>::
- Start numbering the patches at <n> instead of 1.
+`--start-number <n>`::
+ Start numbering the patches at _<n>_ instead of 1.
---numbered-files::
+`--numbered-files`::
Output file names will be a simple number sequence
without the default first line of the commit appended.
--k::
---keep-subject::
+`-k`::
+`--keep-subject`::
Do not strip/add '[PATCH]' from the first line of the
commit log message.
--s::
---signoff::
+`-s`::
+`--signoff`::
Add a `Signed-off-by` trailer to the commit message, using
the committer identity of yourself.
See the signoff option in linkgit:git-commit[1] for more information.
---stdout::
+`--stdout`::
Print all commits to the standard output in mbox format,
instead of creating a file for each one.
---attach[=<boundary>]::
+`--attach[=<boundary>]`::
Create multipart/mixed attachment, the first part of
which is the commit message and the patch itself in the
second part, with `Content-Disposition: attachment`.
---no-attach::
+`--no-attach`::
Disable the creation of an attachment, overriding the
configuration setting.
---inline[=<boundary>]::
+`--inline[=<boundary>]`::
Create multipart/mixed attachment, the first part of
which is the commit message and the patch itself in the
second part, with `Content-Disposition: inline`.
---thread[=<style>]::
---no-thread::
- Controls addition of `In-Reply-To` and `References` headers to
+`--thread[=<style>]`::
+`--no-thread`::
+ Control addition of `In-Reply-To` and `References` headers to
make the second and subsequent mails appear as replies to the
first. Also controls generation of the `Message-ID` header to
reference.
+
-The optional <style> argument can be either `shallow` or `deep`.
-'shallow' threading makes every mail a reply to the head of the
+The optional _<style>_ argument can be either `shallow` or `deep`.
+`shallow` threading makes every mail a reply to the head of the
series, where the head is chosen from the cover letter, the
-`--in-reply-to`, and the first patch mail, in this order. 'deep'
+`--in-reply-to`, and the first patch mail, in this order. `deep`
threading makes every mail a reply to the previous one.
+
The default is `--no-thread`, unless the `format.thread` configuration
is set. `--thread` without an argument is equivalent to `--thread=shallow`.
+
-Beware that the default for 'git send-email' is to thread emails
+Beware that the default for `git send-email` is to thread emails
itself. If you want `git format-patch` to take care of threading, you
will want to ensure that threading is disabled for `git send-email`.
---in-reply-to=<message-id>::
+`--in-reply-to=<message-id>`::
Make the first mail (or all the mails with `--no-thread`) appear as a
- reply to the given <message-id>, which avoids breaking threads to
+ reply to the given _<message-id>_, which avoids breaking threads to
provide a new patch series.
---ignore-if-in-upstream::
+`--ignore-if-in-upstream`::
Do not include a patch that matches a commit in
- <until>..<since>. This will examine all patches reachable
- from <since> but not from <until> and compare them with the
+ _<until>_.._<since>_. This will examine all patches reachable
+ from _<since>_ but not from _<until>_ and compare them with the
patches being generated, and any patch that matches is
ignored.
---always::
+`--always`::
Include patches for commits that do not introduce any change,
which are omitted by default.
---cover-from-description=<mode>::
- Controls which parts of the cover letter will be automatically
+`--cover-from-description=<mode>`::
+ Control which parts of the cover letter will be automatically
populated using the branch's description.
+
-If `<mode>` is `message` or `default`, the cover letter subject will be
+_<mode>_ can have the following values:
+
+`message`;;
+`default`;; the cover letter subject will be
populated with placeholder text. The body of the cover letter will be
populated with the branch's description. This is the default mode when
no configuration nor command line option is specified.
-+
-If `<mode>` is `subject`, the first paragraph of the branch description will
+`subject`;; the first paragraph of the branch description will
populate the cover letter subject. The remainder of the description will
populate the body of the cover letter.
-+
-If `<mode>` is `auto`, if the first paragraph of the branch description
+`auto`;; if the first paragraph of the branch description
is greater than 100 bytes, then the mode will be `message`, otherwise
`subject` will be used.
-+
-If `<mode>` is `none`, both the cover letter subject and body will be
+`none`;; both the cover letter subject and body will be
populated with placeholder text.
---description-file=<file>::
- Use the contents of <file> instead of the branch's description
+`--description-file=<file>`::
+ Use the contents of _<file>_ instead of the branch's description
for generating the cover letter.
---subject-prefix=<subject-prefix>::
+`--subject-prefix=<subject-prefix>`::
Use '[<subject-prefix>]' instead of the standard '[PATCH]'
prefix in the subject line. This can be used to name a patch
series, and can be combined with the `--numbered` option.
@@ -231,14 +231,14 @@ all patches. This is often useful on mailing lists which receive
patches for several repositories and can be used to disambiguate
the patches (with a value of e.g. "PATCH my-project").
---filename-max-length=<n>::
+`--filename-max-length=<n>`::
Instead of the standard 64 bytes, chomp the generated output
- filenames at around '<n>' bytes (too short a value will be
+ filenames at around _<n>_ bytes (too short a value will be
silently raised to a reasonable length). Defaults to the
value of the `format.filenameMaxLength` configuration
variable, or 64 if unconfigured.
---rfc[=<rfc>]::
+`--rfc[=<rfc>]`::
Prepends the string _<rfc>_ (defaults to "RFC") to
the subject prefix. As the subject prefix defaults to
"PATCH", you'll get "RFC PATCH" by default.
@@ -254,38 +254,38 @@ can be prefixed with a dash ("`-`") to signal that the rest of
the _<rfc>_ string should be appended to the subject prefix instead,
e.g., `--rfc='-(WIP)'` results in "PATCH (WIP)".
--v <n>::
---reroll-count=<n>::
- Mark the series as the <n>-th iteration of the topic. The
+`-v <n>`::
+`--reroll-count=<n>`::
+ Mark the series as the _<n>_-th iteration of the topic. The
output filenames have `v<n>` prepended to them, and the
subject prefix ("PATCH" by default, but configurable via the
- `--subject-prefix` option) has ` v<n>` appended to it. E.g.
+ `--subject-prefix` option) has `v<n>` appended to it. E.g.
`--reroll-count=4` may produce `v4-0001-add-makefile.patch`
file that has "Subject: [PATCH v4 1/20] Add makefile" in it.
- `<n>` does not have to be an integer (e.g. "--reroll-count=4.4",
- or "--reroll-count=4rev2" are allowed), but the downside of
+ _<n>_ does not have to be an integer (e.g. `--reroll-count=4.4`,
+ or `--reroll-count=4rev2` are allowed), but the downside of
using such a reroll-count is that the range-diff/interdiff
with the previous version does not state exactly which
version the new iteration is compared against.
---to=<email>::
+`--to=<email>`::
Add a `To:` header to the email headers. This is in addition
to any configured headers, and may be used multiple times.
The negated form `--no-to` discards all `To:` headers added so
far (from config or command line).
---cc=<email>::
+`--cc=<email>`::
Add a `Cc:` header to the email headers. This is in addition
to any configured headers, and may be used multiple times.
The negated form `--no-cc` discards all `Cc:` headers added so
far (from config or command line).
---from::
---from=<ident>::
- Use `ident` in the `From:` header of each email. In case of a
+`--from`::
+`--from=<ident>`::
+ Use _<ident>_ in the `From:` header of each email. In case of a
commit email, if the author ident of the commit is not textually
- identical to the provided `ident`, place a `From:` header in the
- body of the message with the original author. If no `ident` is
+ identical to the provided _<ident>_, place a `From:` header in the
+ body of the message with the original author. If no _<ident>_ is
given, or if the option is not passed at all, use the ident of
the current committer.
+
@@ -296,8 +296,8 @@ header). Note also that `git send-email` already handles this
transformation for you, and this option should not be used if you are
feeding the result to `git send-email`.
---force-in-body-from::
---no-force-in-body-from::
+`--force-in-body-from`::
+`--no-force-in-body-from`::
With the e-mail sender specified via the `--from` option, by
default, an in-body "From:" to identify the real author of
the commit is added at the top of the commit log message if
@@ -308,7 +308,7 @@ feeding the result to `git send-email`.
Defaults to the value of the `format.forceInBodyFrom`
configuration variable.
---add-header=<header>::
+`--add-header=<header>`::
Add an arbitrary header to the email headers. This is in addition
to any configured headers, and may be used multiple times.
For example, `--add-header="Organization: git-foo"`.
@@ -316,13 +316,13 @@ feeding the result to `git send-email`.
`Cc:`, and custom) headers added so far from config or command
line.
---cover-letter::
---no-cover-letter::
+`--cover-letter`::
+`--no-cover-letter`::
In addition to the patches, generate a cover letter file containing the
branch description, commit list and the overall diffstat. You can fill
in a description in the file before sending it out.
---commit-list-format=<format-spec>::
+`--commit-list-format=<format-spec>`::
Specify the format in which to generate the commit list of the patch
series. The accepted values for format-spec are `shortlog`, `modern` or
a format-string prefixed with `log:`. E.g. `log: %s (%an)`.
@@ -334,28 +334,28 @@ feeding the result to `git send-email`.
This option given from the command-line implies the use of
`--cover-letter` unless `--no-cover-letter` is given.
---encode-email-headers::
---no-encode-email-headers::
+`--encode-email-headers`::
+`--no-encode-email-headers`::
Encode email headers that have non-ASCII characters with
"Q-encoding" (described in RFC 2047), instead of outputting the
headers verbatim. Defaults to the value of the
`format.encodeEmailHeaders` configuration variable.
---interdiff=<previous>::
+`--interdiff=<previous>`::
As a reviewer aid, insert an interdiff into the cover letter,
or as commentary of the lone patch of a 1-patch series, showing
the differences between the previous version of the patch series and
- the series currently being formatted. `previous` is a single revision
+ the series currently being formatted. _<previous>_ is a single revision
naming the tip of the previous series which shares a common base with
the series being formatted (for example `git format-patch
--cover-letter --interdiff=feature/v1 -3 feature/v2`).
---range-diff=<previous>::
+`--range-diff=<previous>`::
As a reviewer aid, insert a range-diff (see linkgit:git-range-diff[1])
into the cover letter, or as commentary of the lone patch of a
1-patch series, showing the differences between the previous
version of the patch series and the series currently being formatted.
- `previous` can be a single revision naming the tip of the previous
+ _<previous>_ can be a single revision naming the tip of the previous
series if it shares a common base with the series being formatted (for
example `git format-patch --cover-letter --range-diff=feature/v1 -3
feature/v2`), or a revision range if the two versions of the series are
@@ -367,7 +367,7 @@ product of `format-patch` is generated, and they are not passed to
the underlying `range-diff` machinery used to generate the cover-letter
material (this may change in the future).
---creation-factor=<percent>::
+`--creation-factor=<percent>`::
Used with `--range-diff`, tweak the heuristic which matches up commits
between the previous and current series of patches by adjusting the
creation/deletion cost fudge factor. See linkgit:git-range-diff[1])
@@ -378,8 +378,8 @@ case is to show comparison with an older iteration of the same
topic and the tool should find more correspondence between the two
sets of patches.
---notes[=<ref>]::
---no-notes::
+`--notes[=<ref>]`::
+`--no-notes`::
Append the notes (see linkgit:git-notes[1]) for the commit
after the three-dash line.
+
@@ -394,17 +394,17 @@ configuration options in linkgit:git-notes[1] to use this workflow).
The default is `--no-notes`, unless the `format.notes` configuration is
set.
---signature=<signature>::
---no-signature::
+`--signature=<signature>`::
+`--no-signature`::
Add a signature to each message produced. Per RFC 3676 the signature
- is separated from the body by a line with '-- ' on it. If the
+ is separated from the body by a line with "-- " on it. If the
signature option is omitted the signature defaults to the Git version
number.
---signature-file=<file>::
- Works just like --signature except the signature is read from a file.
+`--signature-file=<file>`::
+ Works just like `--signature` except the signature is read from a file.
---suffix=.<sfx>::
+`--suffix=.<sfx>`::
Instead of using `.patch` as the suffix for generated
filenames, use specified suffix. A common alternative is
`--suffix=.txt`. Leaving this empty will remove the `.patch`
@@ -413,36 +413,36 @@ set.
Note that the leading character does not have to be a dot; for example,
you can use `--suffix=-patch` to get `0001-description-of-my-change-patch`.
--q::
---quiet::
+`-q`::
+`--quiet`::
Do not print the names of the generated files to standard output.
---no-binary::
+`--no-binary`::
Do not output contents of changes in binary files, instead
display a notice that those files changed. Patches generated
using this option cannot be applied properly, but they are
still useful for code review.
---zero-commit::
+`--zero-commit`::
Output an all-zero hash in each patch's From header instead
of the hash of the commit.
---no-base::
---base[=<commit>]::
+`--no-base`::
+`--base[=<commit>]`::
Record the base tree information to identify the state the
patch series applies to. See the BASE TREE INFORMATION section
- below for details. If <commit> is "auto", a base commit is
+ below for details. If _<commit>_ is `auto`, a base commit is
automatically chosen. The `--no-base` option overrides a
`format.useAutoBase` configuration.
---root::
- Treat the revision argument as a <revision-range>, even if it
+`--root`::
+ Treat the revision argument as a _<revision-range>_, even if it
is just a single commit (that would normally be treated as a
- <since>). Note that root commits included in the specified
+ _<since>_). Note that root commits included in the specified
range are always formatted as creation patches, independently
of this flag.
---progress::
+`--progress`::
Show progress reports on stderr as patches are generated.
CONFIGURATION
@@ -473,7 +473,7 @@ with configuration variables.
DISCUSSION
----------
-The patch produced by 'git format-patch' is in UNIX mailbox format,
+The patch produced by `git format-patch` is in UNIX mailbox format,
with a fixed "magic" time stamp to indicate that the file is output
from format-patch rather than a real mailbox, like so:
@@ -502,8 +502,8 @@ can save interesting patches in a UNIX mailbox and apply them with
linkgit:git-am[1].
When a patch is part of an ongoing discussion, the patch generated by
-'git format-patch' can be tweaked to take advantage of the 'git am
---scissors' feature. After your response to the discussion comes a
+`git format-patch` can be tweaked to take advantage of the `git am
+--scissors` feature. After your response to the discussion comes a
line that consists solely of "`-- >8 --`" (scissors and perforation),
followed by the patch with unnecessary header fields removed:
@@ -540,7 +540,7 @@ two common types of corruption:
One way to test if your MUA is set up correctly is:
* Send the patch to yourself, exactly the way you would, except
- with To: and Cc: lines that do not contain the list and
+ with `To:` and `Cc:` lines that do not contain the list and
maintainer address.
* Save that patch to a file in UNIX mailbox format. Call it a.patch,
@@ -561,12 +561,12 @@ If it does not apply correctly, there can be various reasons.
this case.
* The MUA corrupted your patch; "am" would complain that
- the patch does not apply. Look in the .git/rebase-apply/ subdirectory and
- see what 'patch' file contains and check for the common
- corruption patterns mentioned above.
+ the patch does not apply. Look in the `.git/rebase-apply/` subdirectory and
+ see what `patch` file contains and check for the common
+ corruption patterns mentioned above.
-* While at it, check the 'info' and 'final-commit' files as well.
- If what is in 'final-commit' is not exactly what you would want to
+* While at it, check the `info` and `final-commit` files as well.
+ If what is in `final-commit` is not exactly what you would want to
see in the commit log message, it is very likely that the
receiver would end up hand editing the log message when applying
your patch. Things like "Hi, this is my first patch.\n" in the
@@ -582,11 +582,11 @@ GMail
~~~~~
GMail does not have any way to turn off line wrapping in the web
interface, so it will mangle any emails that you send. You can however
-use "git send-email" and send your patches through the GMail SMTP server, or
+use `git send-email` and send your patches through the GMail SMTP server, or
use any IMAP email client to connect to the google IMAP server and forward
the emails through that.
-For hints on using 'git send-email' to send your patches through the
+For hints on using `git send-email` to send your patches through the
GMail SMTP server, see the EXAMPLE section of linkgit:git-send-email[1].
For hints on submission using the IMAP interface, see the EXAMPLE
@@ -595,7 +595,7 @@ section of linkgit:git-imap-send[1].
Thunderbird
~~~~~~~~~~~
By default, Thunderbird will both wrap emails as well as flag
-them as being 'format=flowed', both of which will make the
+them as being `format=flowed`, both of which will make the
resulting email unusable by Git.
There are three different approaches: use an add-on to turn off line wraps,
@@ -609,14 +609,14 @@ Install the Toggle Line Wrap add-on that is available from
https://addons.thunderbird.net/thunderbird/addon/toggle-line-wrap
It adds a button "Line Wrap" to the composer's toolbar
that you can tick off. Now you can compose the message as you otherwise do
-(cut + paste, 'git format-patch' | 'git imap-send', etc), but you have to
+(cut + paste, `git format-patch` | `git imap-send`, etc), but you have to
insert line breaks manually in any text that you type.
As a bonus feature, the add-on can detect patch text in the composer
and warns when line wrapping has not yet been turned off.
The add-on requires a few tweaks of the advanced configuration
-(about:config). These are listed on the download page.
+(`about:config`). These are listed on the download page.
Approach #2 (configuration)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -643,7 +643,7 @@ Toggle it to make sure it is set to `false`. Also, search for
Toggle it to make sure it is set to `false`.
After that is done, you should be able to compose email as you
-otherwise would (cut + paste, 'git format-patch' | 'git imap-send', etc),
+otherwise would (cut + paste, `git format-patch` | `git imap-send`, etc),
and the patches will not be mangled.
Approach #3 (external editor)
@@ -660,7 +660,7 @@ External Editor from https://globs.org/articles.php?lng=en&pg=8
"Composition & Addressing" panel of the account to be used to
send the patch.
-3. In the main Thunderbird window, 'before' you open the compose
+3. In the main Thunderbird window, _before_ you open the compose
window for the patch, use Tools->about:config to set the
following to the indicated values:
+
@@ -708,15 +708,15 @@ BASE TREE INFORMATION
The base tree information block is used for maintainers or third party
testers to know the exact state the patch series applies to. It consists
-of the 'base commit', which is a well-known commit that is part of the
+of the "base commit", which is a well-known commit that is part of the
stable part of the project history everybody else works off of, and zero
-or more 'prerequisite patches', which are well-known patches in flight
-that is not yet part of the 'base commit' that need to be applied on top
-of 'base commit' in topological order before the patches can be applied.
+or more "prerequisite patches", which are well-known patches in flight
+that is not yet part of the "base commit" that need to be applied on top
+of "base commit" in topological order before the patches can be applied.
-The 'base commit' is shown as "base-commit: " followed by the 40-hex of
-the commit object name. A 'prerequisite patch' is shown as
-"prerequisite-patch-id: " followed by the 40-hex 'patch id', which can
+The "base commit" is shown as "base-commit: " followed by the 40-hex of
+the commit object name. A `prerequisite patch` is shown as
+"prerequisite-patch-id: " followed by the 40-hex `patch id`, which can
be obtained by passing the patch through the `git patch-id --stable`
command.
@@ -763,7 +763,7 @@ EXAMPLES
--------
* Extract commits between revisions R1 and R2, and apply them on top of
- the current branch using 'git am' to cherry-pick them:
+ the current branch using `git am` to cherry-pick them:
+
------------
$ git format-patch -k --stdout R1..R2 | git am -3 -k
@@ -778,7 +778,7 @@ $ git format-patch origin
+
For each commit a separate file is created in the current directory.
-* Extract all commits that lead to 'origin' since the inception of the
+* Extract all commits that lead to `origin` since the inception of the
project:
+
------------
@@ -794,7 +794,7 @@ $ git format-patch -M -B origin
Additionally, it detects and handles renames and complete rewrites
intelligently to produce a renaming patch. A renaming patch reduces
the amount of text output, and generally makes it easier to review.
-Note that non-Git "patch" programs won't understand renaming patches, so
+Note that non-Git `patch` programs won't understand renaming patches, so
use it only when you know the recipient uses Git to apply your patch.
* Extract three topmost commits from the current branch and format them
--
gitgitgadget
^ permalink raw reply related
* [PATCH 3/4] doc: convert git-send-email synopsis and options to new style
From: Jean-Noël Avila via GitGitGadget @ 2026-07-19 19:54 UTC (permalink / raw)
To: git; +Cc: Jean-Noël Avila, Jean-Noël Avila
In-Reply-To: <pull.2185.git.1784490878.gitgitgadget@gmail.com>
From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= <jn.avila@free.fr>
Replace [verse] with [synopsis] in the SYNOPSIS block and remove
single-quote formatting from the command name.
Backtick-quote all option terms in the OPTIONS section, convert
the standalone placeholder _<encoding>_ in prose, fix single-quoted
server names to backtick form, and convert mbox(5) to `mbox`(5).
Also update config/sendemail.adoc: backtick-quote all configuration
key terms and fix a bare sendemail.aliasesFile reference in prose.
Signed-off-by: Jean-Noël Avila <jn.avila@free.fr>
---
Documentation/config/sendemail.adoc | 92 ++++++++--------
Documentation/git-send-email.adoc | 162 ++++++++++++++--------------
2 files changed, 127 insertions(+), 127 deletions(-)
diff --git a/Documentation/config/sendemail.adoc b/Documentation/config/sendemail.adoc
index 1d700559b4..5499f91036 100644
--- a/Documentation/config/sendemail.adoc
+++ b/Documentation/config/sendemail.adoc
@@ -1,25 +1,25 @@
-sendemail.identity::
+`sendemail.identity`::
A configuration identity. When given, causes values in the
`sendemail.<identity>` subsection to take precedence over
values in the `sendemail` section. The default identity is
the value of `sendemail.identity`.
-sendemail.smtpEncryption::
+`sendemail.smtpEncryption`::
See linkgit:git-send-email[1] for description. Note that this
setting is not subject to the `identity` mechanism.
-sendemail.smtpSSLCertPath::
+`sendemail.smtpSSLCertPath`::
Path to ca-certificates (either a directory or a single file).
Set it to an empty string to disable certificate verification.
-sendemail.smtpSSLClientCert::
+`sendemail.smtpSSLClientCert`::
Path to the client certificate file to present if requested by the
server. This is required when the server is set up to verify client
certificates. If the corresponding private key is not included in the
file, it must be supplied using `sendemail.smtpSSLClientKey` or the
`--smtp-ssl-client-key` option.
-sendemail.smtpSSLClientKey::
+`sendemail.smtpSSLClientKey`::
Path to the client private key file that corresponds to the client
certificate. To avoid misconfiguration, this configuration must be used
in conjunction with `sendemail.smtpSSLClientCert` or the
@@ -28,45 +28,45 @@ sendemail.smtpSSLClientKey::
the certificate. Visit https://metacpan.org/pod/IO::Socket::SSL for more
details.
-sendemail.<identity>.*::
- Identity-specific versions of the `sendemail.*` parameters
+`sendemail.<identity>.<config>`::
+ Identity-specific versions of the `sendemail.<config>` parameters
found below, taking precedence over those when this
identity is selected, through either the command-line or
`sendemail.identity`.
-sendemail.multiEdit::
+`sendemail.multiEdit`::
If `true` (default), a single editor instance will be spawned to edit
files you have to edit (patches when `--annotate` is used, and the
summary when `--compose` is used). If `false`, files will be edited one
after the other, spawning a new editor each time.
-sendemail.confirm::
+`sendemail.confirm`::
Sets the default for whether to confirm before sending. Must be
one of `always`, `never`, `cc`, `compose`, or `auto`. See `--confirm`
in the linkgit:git-send-email[1] documentation for the meaning of these
values.
-sendemail.mailmap::
+`sendemail.mailmap`::
If `true`, makes linkgit:git-send-email[1] assume `--mailmap`,
otherwise assume `--no-mailmap`. `False` by default.
-sendemail.mailmap.file::
+`sendemail.mailmap.file`::
The location of a linkgit:git-send-email[1] specific augmenting
mailmap file. The default mailmap and `mailmap.file` are loaded
first. Thus, entries in this file take precedence over entries in
the default mailmap locations. See linkgit:gitmailmap[5].
-sendemail.mailmap.blob::
+`sendemail.mailmap.blob`::
Like `sendemail.mailmap.file`, but consider the value as a reference
to a blob in the repository. Entries in `sendemail.mailmap.file`
take precedence over entries here. See linkgit:gitmailmap[5].
-sendemail.aliasesFile::
+`sendemail.aliasesFile`::
To avoid typing long email addresses, point this to one or more
email aliases files. You must also supply `sendemail.aliasFileType`.
-sendemail.aliasFileType::
- Format of the file(s) specified in sendemail.aliasesFile. Must be
+`sendemail.aliasFileType`::
+ Format of the file(s) specified in `sendemail.aliasesFile`. Must be
one of `mutt`, `mailrc`, `pine`, `elm`, `gnus`, or `sendmail`.
+
What an alias file in each format looks like can be found in
@@ -75,7 +75,7 @@ differences and limitations from the standard formats are
described below:
+
--
-sendmail;;
+`sendmail`;;
* Quoted aliases and quoted addresses are not supported: lines that
contain a `"` symbol are ignored.
* Redirection to a file (`/path/name`) or pipe (`|command`) is not
@@ -85,54 +85,54 @@ sendmail;;
explicitly unsupported constructs, and any other lines that are not
recognized by the parser.
--
-sendemail.annotate::
-sendemail.bcc::
-sendemail.cc::
-sendemail.ccCmd::
-sendemail.chainReplyTo::
-sendemail.envelopeSender::
-sendemail.from::
-sendemail.headerCmd::
-sendemail.signedOffByCc::
-sendemail.smtpPass::
-sendemail.suppressCc::
-sendemail.suppressFrom::
-sendemail.to::
-sendemail.toCmd::
-sendemail.smtpDomain::
-sendemail.smtpServer::
-sendemail.smtpServerPort::
-sendemail.smtpServerOption::
-sendemail.smtpUser::
-sendemail.imapSentFolder::
-sendemail.useImapOnly::
-sendemail.thread::
-sendemail.transferEncoding::
-sendemail.validate::
-sendemail.xmailer::
+`sendemail.annotate`::
+`sendemail.bcc`::
+`sendemail.cc`::
+`sendemail.ccCmd`::
+`sendemail.chainReplyTo`::
+`sendemail.envelopeSender`::
+`sendemail.from`::
+`sendemail.headerCmd`::
+`sendemail.signedOffByCc`::
+`sendemail.smtpPass`::
+`sendemail.suppressCc`::
+`sendemail.suppressFrom`::
+`sendemail.to`::
+`sendemail.toCmd`::
+`sendemail.smtpDomain`::
+`sendemail.smtpServer`::
+`sendemail.smtpServerPort`::
+`sendemail.smtpServerOption`::
+`sendemail.smtpUser`::
+`sendemail.imapSentFolder`::
+`sendemail.useImapOnly`::
+`sendemail.thread`::
+`sendemail.transferEncoding`::
+`sendemail.validate`::
+`sendemail.xmailer`::
These configuration variables all provide a default for
linkgit:git-send-email[1] command-line options. See its
documentation for details.
-sendemail.outlookidfix::
+`sendemail.outlookidfix`::
If `true`, makes linkgit:git-send-email[1] assume `--outlook-id-fix`,
and if `false` assume `--no-outlook-id-fix`. If not specified, it will
behave the same way as if `--outlook-id-fix` is not specified.
-sendemail.signedOffCc (deprecated)::
+`sendemail.signedOffCc` (deprecated)::
Deprecated alias for `sendemail.signedOffByCc`.
-sendemail.smtpBatchSize::
+`sendemail.smtpBatchSize`::
Number of messages to be sent per connection, after that a relogin
will happen. If the value is `0` or undefined, send all messages in
one connection.
See also the `--batch-size` option of linkgit:git-send-email[1].
-sendemail.smtpReloginDelay::
+`sendemail.smtpReloginDelay`::
Seconds to wait before reconnecting to the smtp server.
See also the `--relogin-delay` option of linkgit:git-send-email[1].
-sendemail.forbidSendmailVariables::
+`sendemail.forbidSendmailVariables`::
To avoid common misconfiguration mistakes, linkgit:git-send-email[1]
will abort with a warning if any configuration options for `sendmail`
exist. Set this variable to bypass the check.
diff --git a/Documentation/git-send-email.adoc b/Documentation/git-send-email.adoc
index dea3b86460..5c9ab39944 100644
--- a/Documentation/git-send-email.adoc
+++ b/Documentation/git-send-email.adoc
@@ -8,11 +8,11 @@ git-send-email - Send a collection of patches as emails
SYNOPSIS
--------
-[verse]
-'git send-email' [<options>] (<file>|<directory>)...
-'git send-email' [<options>] <format-patch-options>
-'git send-email' --dump-aliases
-'git send-email' --translate-aliases
+[synopsis]
+git send-email [<options>] (<file>|<directory>)...
+git send-email [<options>] <format-patch-options>
+git send-email --dump-aliases
+git send-email --translate-aliases
DESCRIPTION
@@ -48,24 +48,24 @@ OPTIONS
Composing
~~~~~~~~~
---annotate::
+`--annotate`::
Review and edit each patch you're about to send. Default is the value
of `sendemail.annotate`. See the CONFIGURATION section for
`sendemail.multiEdit`.
---bcc=<address>,...::
+`--bcc=<address>,...`::
Specify a `Bcc:` value for each email. Default is the value of
`sendemail.bcc`.
+
This option may be specified multiple times.
---cc=<address>,...::
+`--cc=<address>,...`::
Specify a starting `Cc:` value for each email.
Default is the value of `sendemail.cc`.
+
This option may be specified multiple times.
---compose::
+`--compose`::
Invoke a text editor (see GIT_EDITOR in linkgit:git-var[1])
to edit an introductory message for the patch series.
+
@@ -80,7 +80,7 @@ Missing `From` or `In-Reply-To` headers will be prompted for.
+
See the CONFIGURATION section for `sendemail.multiEdit`.
---from=<address>::
+`--from=<address>`::
Specify the sender of the emails. If not specified on the command line,
the value of the `sendemail.from` configuration option is used. If
neither the command-line option nor `sendemail.from` are set, then the
@@ -88,12 +88,12 @@ See the CONFIGURATION section for `sendemail.multiEdit`.
the value of `GIT_AUTHOR_IDENT`, or `GIT_COMMITTER_IDENT` if that is not
set, as returned by `git var -l`.
---reply-to=<address>::
+`--reply-to=<address>`::
Specify the address where replies from recipients should go to.
Use this if replies to messages should go to another address than what
is specified with the `--from` parameter.
---in-reply-to=<identifier>::
+`--in-reply-to=<identifier>`::
Make the first mail (or all the mails with `--no-thread`) appear as a
reply to the given Message-ID, which avoids breaking threads to
provide a new patch series.
@@ -115,8 +115,8 @@ illustration below where `[PATCH v2 0/3]` is in reply to `[PATCH 0/2]`:
Only necessary if `--compose` is also set. If `--compose`
is not set, this will be prompted for.
---outlook-id-fix::
---no-outlook-id-fix::
+`--outlook-id-fix`::
+`--no-outlook-id-fix`::
Microsoft Outlook SMTP servers discard the Message-ID sent via email and
assign a new random Message-ID, thus breaking threads.
+
@@ -126,15 +126,15 @@ threading. Use it only when you know that the server reports the
rewritten Message-ID the same way as Outlook servers do.
+
Without this option specified, the fix is done by default when talking
-to 'smtp.office365.com' or 'smtp-mail.outlook.com'. Use
+to `smtp.office365.com` or `smtp-mail.outlook.com`. Use
`--no-outlook-id-fix` to disable even when talking to these two servers.
---subject=<string>::
+`--subject=<string>`::
Specify the initial subject of the email thread.
Only necessary if `--compose` is also set. If `--compose`
is not set, this will be prompted for.
---to=<address>,...::
+`--to=<address>,...`::
Specify the primary recipient of the emails generated. Generally, this
will be the upstream maintainer of the project involved. Default is the
value of the `sendemail.to` configuration value; if that is unspecified,
@@ -142,20 +142,20 @@ to 'smtp.office365.com' or 'smtp-mail.outlook.com'. Use
+
This option may be specified multiple times.
---8bit-encoding=<encoding>::
+`--8bit-encoding=<encoding>`::
When encountering a non-ASCII message or subject that does not
declare its encoding, add headers/quoting to indicate it is
- encoded in <encoding>. Default is the value of the
+ encoded in _<encoding>_. Default is the value of the
`sendemail.assume8bitEncoding`; if that is unspecified, this
will be prompted for if any non-ASCII files are encountered.
+
Note that no attempts whatsoever are made to validate the encoding.
---compose-encoding=<encoding>::
+`--compose-encoding=<encoding>`::
Specify encoding of compose message. Default is the value of the
`sendemail.composeEncoding`; if that is unspecified, UTF-8 is assumed.
---transfer-encoding=(7bit|8bit|quoted-printable|base64|auto)::
+`--transfer-encoding=(7bit|8bit|quoted-printable|base64|auto)`::
Specify the transfer encoding to be used to send the message over SMTP.
`7bit` will fail upon encountering a non-ASCII message. `quoted-printable`
can be useful when the repository contains files that contain carriage
@@ -167,8 +167,8 @@ Note that no attempts whatsoever are made to validate the encoding.
Default is the value of the `sendemail.transferEncoding` configuration
value; if that is unspecified, default to `auto`.
---xmailer::
---no-xmailer::
+`--xmailer`::
+`--no-xmailer`::
Add (or prevent adding) the `X-Mailer:` header. By default,
the header is added, but it can be turned off by setting the
`sendemail.xmailer` configuration variable to `false`.
@@ -176,7 +176,7 @@ value; if that is unspecified, default to `auto`.
Sending
~~~~~~~
---envelope-sender=<address>::
+`--envelope-sender=<address>`::
Specify the envelope sender used to send the emails.
This is useful if your default address is not the address that is
subscribed to a list. In order to use the `From` address, set the
@@ -185,7 +185,7 @@ Sending
`sendemail.envelopeSender` configuration variable; if that is
unspecified, choosing the envelope sender is left to your MTA.
---sendmail-cmd=<command>::
+`--sendmail-cmd=<command>`::
Specify a command to run to send the email. The command should
be sendmail-like; specifically, it must support the `-i` option.
The command will be executed in the shell if necessary. Default
@@ -193,7 +193,7 @@ Sending
`--smtp-server` is also unspecified, `git send-email` will search
for `sendmail` in `/usr/sbin`, `/usr/lib` and `$PATH`.
---smtp-encryption=<encryption>::
+`--smtp-encryption=<encryption>`::
Specify in what way encrypting begins for the SMTP connection.
Valid values are `ssl` and `tls`. Any other value reverts to plain
(unencrypted) SMTP, which defaults to port 25.
@@ -207,14 +207,14 @@ Sending
documentation or your server configuration to make sure
for your own case. Default is the value of `sendemail.smtpEncryption`.
---smtp-domain=<FQDN>::
+`--smtp-domain=<FQDN>`::
Specify the Fully Qualified Domain Name (FQDN) used in the
HELO/EHLO command to the SMTP server. Some servers require the
FQDN to match your IP address. If not set, `git send-email` attempts
to determine your FQDN automatically. Default is the value of
`sendemail.smtpDomain`.
---smtp-auth=<mechanisms>::
+`--smtp-auth=<mechanisms>`::
Whitespace-separated list of allowed SMTP-AUTH mechanisms. This setting
forces using only the listed mechanisms. Example:
+
@@ -229,7 +229,7 @@ is specified, all mechanisms supported by the SASL library can be used. The
special value `none` maybe specified to completely disable authentication
independently of `--smtp-user`.
---smtp-pass[=<password>]::
+`--smtp-pass[=<password>]`::
Password for SMTP-AUTH. The argument is optional: If no
argument is specified, then the empty string is used as
the password. Default is the value of `sendemail.smtpPass`,
@@ -241,10 +241,10 @@ or on the command line. If a username has been specified (with
specified (with `--smtp-pass` or `sendemail.smtpPass`), then
a password is obtained using linkgit:git-credential[1].
---no-smtp-auth::
+`--no-smtp-auth`::
Disable SMTP authentication. Short hand for `--smtp-auth=none`.
---smtp-server=<host>::
+`--smtp-server=<host>`::
Specify the outgoing SMTP server to use (e.g.
`smtp.example.com` or a raw IP address). If unspecified, and if
`--sendmail-cmd` is also unspecified, the default is to search
@@ -257,7 +257,7 @@ option. This method does not support passing arguments or using plain
command names. For those use cases, consider using `--sendmail-cmd`
instead.
---smtp-server-port=<port>::
+`--smtp-server-port=<port>`::
Specify a port different from the default port (SMTP
servers typically listen to smtp port 25, but may also listen to
submission port 587, or the common SSL smtp port 465);
@@ -265,7 +265,7 @@ instead.
are also accepted. The port can also be set with the
`sendemail.smtpServerPort` configuration variable.
---smtp-server-option=<option>::
+`--smtp-server-option=<option>`::
Specify the outgoing SMTP server option to use.
Default value can be specified by the `sendemail.smtpServerOption`
configuration option.
@@ -274,15 +274,15 @@ The `--smtp-server-option` option must be repeated for each option you want
to pass to the server. Likewise, different lines in the configuration files
must be used for each option.
---smtp-ssl::
+`--smtp-ssl`::
Legacy alias for `--smtp-encryption ssl`.
---smtp-ssl-cert-path <path>::
+`--smtp-ssl-cert-path <path>`::
Path to a store of trusted CA certificates for SMTP SSL/TLS
certificate validation (either a directory that has been processed
by `c_rehash`, or a single file containing one or more PEM format
certificates concatenated together: see the description of the
- `-CAfile` _<file>_ and the `-CApath` _<dir>_ options of
+ `-CAfile <file>` and the `-CApath <dir>` options of
https://docs.openssl.org/master/man1/openssl-verify/
[OpenSSL's verify(1) manual page] for more information on these).
Set it to an empty string to disable certificate verification.
@@ -290,7 +290,7 @@ must be used for each option.
variable, if set, or the backing SSL library's compiled-in default
otherwise (which should be the best choice on most platforms).
---smtp-ssl-client-cert <path>::
+`--smtp-ssl-client-cert <path>`::
Path to the client certificate file to present if requested by the
server. This option is required when the server is set up to verify
client certificates. If the corresponding private key is not included in
@@ -299,7 +299,7 @@ must be used for each option.
to the value of the `sendemail.smtpSSLClientCert` configuration
variable, if set.
---smtp-ssl-client-key <path>::
+`--smtp-ssl-client-key <path>`::
Path to the client private key file that corresponds to the client
certificate. To avoid misconfiguration, this option must be used in
conjunction with the `sendemail.smtpSSLClientKey` configuration variable
@@ -309,17 +309,17 @@ must be used for each option.
more details. Defaults to the value of the `sendemail.smtpSSLClientKey`
configuration variable, if set.
---smtp-user=<user>::
+`--smtp-user=<user>`::
Username for SMTP-AUTH. Default is the value of `sendemail.smtpUser`;
if a username is not specified (with `--smtp-user` or `sendemail.smtpUser`),
then authentication is not attempted.
---smtp-debug=(0|1)::
+`--smtp-debug=(0|1)`::
Enable (1) or disable (0) debug output. If enabled, SMTP
commands and replies will be printed. Useful to debug TLS
connection and authentication problems.
---imap-sent-folder=<folder>::
+`--imap-sent-folder=<folder>`::
Some email providers (e.g. iCloud) do not send a copy of the emails sent
using SMTP to the `Sent` folder or similar in your mailbox. Use this option
to use `git imap-send` to send a copy of the emails to the folder specified
@@ -331,8 +331,8 @@ must be used for each option.
This feature requires setting up `git imap-send`. See linkgit:git-imap-send[1]
for instructions.
---use-imap-only::
---no-use-imap-only::
+`--use-imap-only`::
+`--no-use-imap-only`::
If this is set, all emails will only be copied to the IMAP folder specified
with `--imap-sent-folder` or `sendemail.imapSentFolder` and will not be sent
to the recipients. Useful if you just want to create a draft of the emails
@@ -344,8 +344,8 @@ for instructions.
This feature requires setting up `git imap-send`. See linkgit:git-imap-send[1]
for instructions.
---batch-size=<num>::
- Some email servers (e.g. 'smtp.163.com') limit the number of emails to be
+`--batch-size=<num>`::
+ Some email servers (e.g. `smtp.163.com`) limit the number of emails to be
sent per session (connection) and this will lead to a failure when
sending many messages. With this option, send-email will disconnect after
sending _<num>_ messages and wait for a few seconds
@@ -354,7 +354,7 @@ for instructions.
retype your password every time this happens. Defaults to the
`sendemail.smtpBatchSize` configuration variable.
---relogin-delay=<int>::
+`--relogin-delay=<int>`::
Waiting _<int>_ seconds before reconnecting to SMTP server. Used together
with `--batch-size` option. Defaults to the `sendemail.smtpReloginDelay`
configuration variable.
@@ -362,29 +362,29 @@ for instructions.
Automating
~~~~~~~~~~
---no-to::
---no-cc::
---no-bcc::
+`--no-to`::
+`--no-cc`::
+`--no-bcc`::
Clear any list of `To:`, `Cc:`, `Bcc:` addresses previously
set via config.
---no-identity::
+`--no-identity`::
Clear the previously read value of `sendemail.identity` set
via config, if any.
---to-cmd=<command>::
+`--to-cmd=<command>`::
Specify a command to execute once per patch file which
should generate patch file specific `To:` entries.
Output of this command must be single email address per line.
Default is the value of `sendemail.toCmd` configuration value.
---cc-cmd=<command>::
+`--cc-cmd=<command>`::
Specify a command to execute once per patch file which
should generate patch file specific `Cc:` entries.
Output of this command must be single email address per line.
Default is the value of `sendemail.ccCmd` configuration value.
---header-cmd=<command>::
+`--header-cmd=<command>`::
Specify a command that is executed once per outgoing message
and output RFC 2822 style header lines to be inserted into
them. When the `sendemail.headerCmd` configuration variable is
@@ -392,11 +392,11 @@ Automating
at the command line, its value takes precedence over the
`sendemail.headerCmd` configuration variable.
---no-header-cmd::
+`--no-header-cmd`::
Disable any header command in use.
---chain-reply-to::
---no-chain-reply-to::
+`--chain-reply-to`::
+`--no-chain-reply-to`::
If this is set, each email will be sent as a reply to the previous
email sent. If disabled with `--no-chain-reply-to`, all emails after
the first will be sent as replies to the first email sent. When using
@@ -404,34 +404,34 @@ Automating
entire patch series. Disabled by default, but the `sendemail.chainReplyTo`
configuration variable can be used to enable it.
---identity=<identity>::
+`--identity=<identity>`::
A configuration identity. When given, causes values in the
`sendemail.<identity>` subsection to take precedence over
values in the `sendemail` section. The default identity is
the value of `sendemail.identity`.
---signed-off-by-cc::
---no-signed-off-by-cc::
+`--signed-off-by-cc`::
+`--no-signed-off-by-cc`::
If this is set, add emails found in the `Signed-off-by` trailer or `Cc:`
lines to the cc list. Default is the value of `sendemail.signedOffByCc`
configuration value; if that is unspecified, default to
`--signed-off-by-cc`.
---cc-cover::
---no-cc-cover::
+`--cc-cover`::
+`--no-cc-cover`::
If this is set, emails found in `Cc:` headers in the first patch of
the series (typically the cover letter) are added to the cc list
for each email set. Default is the value of `sendemail.ccCover`
configuration value; if that is unspecified, default to `--no-cc-cover`.
---to-cover::
---no-to-cover::
+`--to-cover`::
+`--no-to-cover`::
If this is set, emails found in `To:` headers in the first patch of
the series (typically the cover letter) are added to the to list
for each email set. Default is the value of `sendemail.toCover`
configuration value; if that is unspecified, default to `--no-to-cover`.
---suppress-cc=<category>::
+`--suppress-cc=<category>`::
Specify an additional category of recipients to suppress the
auto-cc of:
+
@@ -456,14 +456,14 @@ Default is the value of `sendemail.suppressCc` configuration value; if
that is unspecified, default to `self` if `--suppress-from` is
specified, as well as `body` if `--no-signed-off-by-cc` is specified.
---suppress-from::
---no-suppress-from::
+`--suppress-from`::
+`--no-suppress-from`::
If this is set, do not add the `From:` address to the `Cc:` list.
Default is the value of `sendemail.suppressFrom` configuration
value; if that is unspecified, default to `--no-suppress-from`.
---thread::
---no-thread::
+`--thread`::
+`--no-thread`::
If this is set, the `In-Reply-To` and `References` headers will be
added to each email sent. Whether each mail refers to the
previous email (`deep` threading per `git format-patch`
@@ -481,8 +481,8 @@ exists when `git send-email` is asked to add it (especially note that
Failure to do so may not produce the expected result in the
recipient's MUA.
---mailmap::
---no-mailmap::
+`--mailmap`::
+`--no-mailmap`::
Use the mailmap file (see linkgit:gitmailmap[5]) to map all
addresses to their canonical real name and email address. Additional
mailmap data specific to `git send-email` may be provided using the
@@ -492,7 +492,7 @@ recipient's MUA.
Administering
~~~~~~~~~~~~~
---confirm=<mode>::
+`--confirm=<mode>`::
Confirm just before sending:
+
--
@@ -508,22 +508,22 @@ Default is the value of `sendemail.confirm` configuration value; if that
is unspecified, default to `auto` unless any of the suppress options
have been specified, in which case default to `compose`.
---dry-run::
+`--dry-run`::
Do everything except actually send the emails.
---format-patch::
---no-format-patch::
+`--format-patch`::
+`--no-format-patch`::
When an argument may be understood either as a reference or as a file name,
choose to understand it as a format-patch argument (`--format-patch`)
or as a file name (`--no-format-patch`). By default, when such a conflict
occurs, `git send-email` will fail.
---quiet::
+`--quiet`::
Make `git send-email` less verbose. One line per email should be
all that is output.
---validate::
---no-validate::
+`--validate`::
+`--no-validate`::
Perform sanity checks on patches.
Currently, validation means the following:
+
@@ -539,20 +539,20 @@ have been specified, in which case default to `compose`.
Default is the value of `sendemail.validate`; if this is not set,
default to `--validate`.
---force::
+`--force`::
Send emails even if safety checks would prevent it.
Information
~~~~~~~~~~~
---dump-aliases::
+`--dump-aliases`::
Instead of the normal operation, dump the shorthand alias names from
the configured alias file(s), one per line in alphabetical order. Note
that this only includes the alias name and not its expanded email addresses.
See `sendemail.aliasesFile` for more information about aliases.
---translate-aliases::
+`--translate-aliases`::
Instead of the normal operation, read from standard input and
interpret each line as an email alias. Translate it according to the
configured alias file(s). Output each translated name and email
@@ -718,7 +718,7 @@ include::format-patch-caveats.adoc[]
SEE ALSO
--------
-linkgit:git-format-patch[1], linkgit:git-imap-send[1], mbox(5)
+linkgit:git-format-patch[1], linkgit:git-imap-send[1], `mbox`(5)
GIT
---
--
gitgitgadget
^ permalink raw reply related
* [PATCH 4/4] doc: convert git-request-pull synopsis and options to new style
From: Jean-Noël Avila via GitGitGadget @ 2026-07-19 19:54 UTC (permalink / raw)
To: git; +Cc: Jean-Noël Avila, Jean-Noël Avila
In-Reply-To: <pull.2185.git.1784490878.gitgitgadget@gmail.com>
From: =?UTF-8?q?Jean-No=C3=ABl=20Avila?= <jn.avila@free.fr>
* Replace [verse] with [synopsis] in the SYNOPSIS block
* remove single-quote formatting from the command name
* backtick-quote the flag
* use _<placeholder>_ form for positional parameter terms
Signed-off-by: Jean-Noël Avila <jn.avila@free.fr>
---
Documentation/git-request-pull.adoc | 38 +++++++++++++++++------------
1 file changed, 23 insertions(+), 15 deletions(-)
diff --git a/Documentation/git-request-pull.adoc b/Documentation/git-request-pull.adoc
index 15dcbb6d91..8b6642b312 100644
--- a/Documentation/git-request-pull.adoc
+++ b/Documentation/git-request-pull.adoc
@@ -7,8 +7,8 @@ git-request-pull - Generates a summary of pending changes
SYNOPSIS
--------
-[verse]
-'git request-pull' [-p] <start> <URL> [<end>]
+[synopsis]
+git request-pull [-p] <start> <URL> [<end>]
DESCRIPTION
-----------
@@ -19,28 +19,28 @@ begins with the branch description, summarizes
the changes, and indicates from where they can be pulled.
The upstream project is expected to have the commit named by
-`<start>` and the output asks it to integrate the changes you made
-since that commit, up to the commit named by `<end>`, by visiting
-the repository named by `<URL>`.
+_<start>_ and the output asks it to integrate the changes you made
+since that commit, up to the commit named by _<end>_, by visiting
+the repository named by _<URL>_.
OPTIONS
-------
--p::
+`-p`::
Include patch text in the output.
-<start>::
+_<start>_::
Commit to start at. This names a commit that is already in
the upstream history.
-<URL>::
+_<URL>_::
The repository URL to be pulled from.
-<end>::
- Commit to end at (defaults to HEAD). This names the commit
+_<end>_::
+ Commit to end at (defaults to `HEAD`). This names the commit
at the tip of the history you are asking to be pulled.
+
-When the repository named by `<URL>` has the commit at a tip of a
+When the repository named by _<URL>_ has the commit at a tip of a
ref that is different from the ref you have locally, you can use the
`<local>:<remote>` syntax, to have its local name, a colon `:`, and
its remote name.
@@ -54,11 +54,15 @@ the `v1.0` release, and want it to be integrated into the project.
First you push that change to your public repository for others to
see:
- git push https://git.ko.xz/project master
+-----
+git push https://git.ko.xz/project master
+-----
Then, you run this command:
- git request-pull v1.0 https://git.ko.xz/project master
+------
+git request-pull v1.0 https://git.ko.xz/project master
+------
which will produce a request to the upstream, summarizing the
changes between the `v1.0` release and your `master`, to pull it
@@ -67,11 +71,15 @@ from your public repository.
If you pushed your change to a branch whose name is different from
the one you have locally, e.g.
- git push https://git.ko.xz/project master:for-linus
+-----
+git push https://git.ko.xz/project master:for-linus
+-----
then you can ask that to be pulled with
- git request-pull v1.0 https://git.ko.xz/project master:for-linus
+-----
+git request-pull v1.0 https://git.ko.xz/project master:for-linus
+-----
GIT
--
gitgitgadget
^ permalink raw reply related
* Re: [PATCH v19 5/7] branch: add --delete-merged <branch>
From: Junio C Hamano @ 2026-07-19 21:42 UTC (permalink / raw)
To: Harald Nordgren via GitGitGadget
Cc: git, Kristoffer Haugsbakk, Johannes Sixt, Phillip Wood,
Harald Nordgren
In-Reply-To: <a6caa5b397da8ea24eb97e6aa6dc92b437e456ef.1784053493.git.gitgitgadget@gmail.com>
"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> diff --git a/builtin/branch.c b/builtin/branch.c
> ...
> +struct spare_data {
> + struct strset *deletable;
> + struct strset *spared;
> +};
Let me offer a brief comment on the data representation chosen for
this design, which initially left me confused enough to suspect a
bug or two. It turns out the confusion was entirely mine, and I
have since convinced myself that the approach is sound.
> +/*
> + * A surviving branch stacked on a deletion candidate would lose its
> + * upstream, so drop that candidate from the delete set and remember it
> + * in "spared" so its own upstream can be tidied up afterwards.
> + */
> +static int spare_stacked_base(const struct reference *ref, void *cb_data)
> +{
> + struct spare_data *data = cb_data;
> + struct branch *branch;
> + const char *upstream, *up_short;
> +
> + if (strset_contains(data->deletable, ref->name))
> + return 0;
> + branch = branch_get(ref->name);
Here, spare_stacked_base() is a callback triggered by the
refs_for_each_branch_ref() iterator. I initially misremembered what
the for-each-ref family of iterators passes to its callbacks. I
thought 'ref->name' here would be a full refname, such as
'refs/heads/main', which does not match what 'branch_get' expects
(which is a branch name). The same confusion led me to think the
'deletable' strset was indexed by full refnames, which would then
...
> + upstream = branch_get_upstream(branch, NULL);
> + if (!upstream || !skip_prefix(upstream, "refs/heads/", &up_short) ||
> + !strset_contains(data->deletable, up_short))
> + return 0;
... mean that this lookup using 'up_short' (the branch name obtained
after stripping the 'refs/heads/' prefix) is buggy. But that is not
the case. The 'deletable' strset stores branch names, so indexing
with 'up_short' is correct, and ...
> + strset_remove(data->deletable, up_short);
> + strset_add(data->spared, up_short);
... adding 'up_short' to the 'deletable' strset is correct too. The
same applies to the 'spared' strset. It is consistently indexed by
branch names rather than full refnames.
This design choice makes perfect sense for this application. We
have no business touching the upstream of a branch unless it is a
local branch. A remote-tracking branch, such as
'refs/remotes/origin/main', lives outside the 'refs/heads/'
hierarchy. Such a branch has no need to interact with either the
'deletable' or 'spared' tables.
I hope others will not be as easily confused as I was, but just in
case it helps future readers ...
Thanks.
^ permalink raw reply
* Re: [PATCH 2/4] doc: convert git-format-patch synopsis and options to new style
From: Junio C Hamano @ 2026-07-19 23:40 UTC (permalink / raw)
To: Jean-Noël Avila via GitGitGadget; +Cc: git, Jean-Noël Avila
In-Reply-To: <e1ed85e3f2733f0f1cc46416903d1e0c8b4c1856.1784490878.git.gitgitgadget@gmail.com>
"Jean-Noël Avila via GitGitGadget" <gitgitgadget@gmail.com> writes:
> Backtick-quote all option terms in the OPTIONS section, convert
> standalone placeholders to _<placeholder>_ form, and convert
> single-quoted commands and tools in prose to backtick form.
OK.
> @@ -708,15 +708,15 @@ BASE TREE INFORMATION
>
> The base tree information block is used for maintainers or third party
> testers to know the exact state the patch series applies to. It consists
> +of the "base commit", which is a well-known commit that is part of the
> stable part of the project history everybody else works off of, and zero
> +or more "prerequisite patches", which are well-known patches in flight
> +that is not yet part of the "base commit" that need to be applied on top
> +of "base commit" in topological order before the patches can be applied.
GIven that the last part of this hunk below uses backtick-quoting
for `prerequisite patch` and `patch id`, shouldn't the references to
`base commit`, and `prerequisite patch(es)` in the above also be
backtick quoted for consistency?
> +The "base commit" is shown as "base-commit: " followed by the 40-hex of
> +the commit object name. A `prerequisite patch` is shown as
> +"prerequisite-patch-id: " followed by the 40-hex `patch id`, which can
> be obtained by passing the patch through the `git patch-id --stable`
> command.
^ permalink raw reply
* Re: [PATCH 4/4] doc: convert git-request-pull synopsis and options to new style
From: Junio C Hamano @ 2026-07-19 23:47 UTC (permalink / raw)
To: Jean-Noël Avila via GitGitGadget; +Cc: git, Jean-Noël Avila
In-Reply-To: <5b6e9bd3f3569cf9977d1cfd120a92756ec86b29.1784490878.git.gitgitgadget@gmail.com>
"Jean-Noël Avila via GitGitGadget" <gitgitgadget@gmail.com> writes:
> @@ -54,11 +54,15 @@ the `v1.0` release, and want it to be integrated into the project.
> First you push that change to your public repository for others to
> see:
>
> - git push https://git.ko.xz/project master
> +-----
> +git push https://git.ko.xz/project master
> +-----
>
> Then, you run this command:
>
> - git request-pull v1.0 https://git.ko.xz/project master
> +------
> +git request-pull v1.0 https://git.ko.xz/project master
> +------
>
> which will produce a request to the upstream, summarizing the
> changes between the `v1.0` release and your `master`, to pull it
> @@ -67,11 +71,15 @@ from your public repository.
> If you pushed your change to a branch whose name is different from
> the one you have locally, e.g.
>
> - git push https://git.ko.xz/project master:for-linus
> +-----
> +git push https://git.ko.xz/project master:for-linus
> +-----
>
> then you can ask that to be pulled with
>
> - git request-pull v1.0 https://git.ko.xz/project master:for-linus
> +-----
> +git request-pull v1.0 https://git.ko.xz/project master:for-linus
> +-----
Is there a widely accepted guideline among AsciiDoc users governing
how many dashes should delimit these blocks, other than "at least
four, with the opening and closing counts matching"? If so, what is
it? We see five, six, five, and five dashes in the proposed changes
above, and in '[PATCH 1/4]' we saw nine. Even if varying counts are
functionally equivalent, the inconsistency is a bit distracting.
Thanks.
[Footnote]
* an excerpt from [PATCH 1/4]
diff --git a/Documentation/git-imap-send.adoc b/Documentation/git-imap-send.adoc
index 538b91afc0..dd1e0a3718 100644
--- a/Documentation/git-imap-send.adoc
+++ b/Documentation/git-imap-send.adoc
@@ -192,7 +192,10 @@ supports only `XOAUTH2` as the mechanism.
Once the commits are ready to be sent, run the following command:
- $ git format-patch --cover-letter -M --stdout origin/master | git imap-send
+
+---------
+$ git format-patch --cover-letter -M --stdout origin/master | git imap-send
+---------
^ permalink raw reply related
* Re: [GSoC Patch v2 6/7] repo: add path.grafts with absolute and relative suffix formatting
From: Lucas Seiki Oshiro @ 2026-07-20 0:20 UTC (permalink / raw)
To: K Jayatheerth; +Cc: git, jltobler, Junio C Hamano
In-Reply-To: <20260717133015.32040-7-jayatheerthkulkarni2005@gmail.com>
> Introduce `path.grafts.absolute` and `path.grafts.relative` keys to
> `git repo info`. This allows scripting layers to query the active grafts
> context cleanly while scaling transparently with active `GIT_GRAFT_FILE`
> environment variable overrides.
I ran `git repo info path.grafts.relative` in a repository with no
`grafts` file, and it returned `.git/info/grafts`, which obviously
doesn't exist.
Wouldn't it be better if we check if that file exists before
returning this value?
^ permalink raw reply
* Re: [GSoC Patch v2 5/7] repo: add path.index with absolute and relative suffix formatting
From: Lucas Seiki Oshiro @ 2026-07-20 0:35 UTC (permalink / raw)
To: K Jayatheerth; +Cc: git, jltobler, Junio C Hamano
In-Reply-To: <20260717133015.32040-6-jayatheerthkulkarni2005@gmail.com>
> Introduce `path.index.absolute` and `path.index.relative` keys to
> `git repo info`. This allows tooling utilities to discover the active
> index context cleanly while scaling transparently with localized
> `GIT_INDEX_FILE` environment overrides.
Note that bare repositories doesn't need to have an index file. Maybe
it would be better if you return an empty value in those cases.
^ permalink raw reply
* Re: [PATCH v2 3/3] bisect: add --auto-reset to leave when done
From: Junio C Hamano @ 2026-07-20 1:14 UTC (permalink / raw)
To: Johannes Sixt; +Cc: Harald Nordgren, Harald Nordgren via GitGitGadget, git
In-Reply-To: <b79a479b-d279-4ac9-a368-6eb8edfed937@kdbg.org>
Johannes Sixt <j6t@kdbg.org> writes:
> IMHO, --auto-reset is too generic and doesn't give a clue what is
> automatic about it. How about --reset-when-found?
I like it already.
Or you can generalize to have --exec-at-end="git bisect reset"
perhaps (ducks and runs)?
;-).
^ permalink raw reply
* Re: [PATCH] rebase: mention --abort alongside --continue
From: Junio C Hamano @ 2026-07-20 2:29 UTC (permalink / raw)
To: Phillip Wood; +Cc: Harald Nordgren, Harald Nordgren via GitGitGadget, git
In-Reply-To: <9a0b542c-8984-4d73-8e24-00adf6dcd427@gmail.com>
Phillip Wood <phillip.wood123@gmail.com> writes:
>>> Perhaps the user is not interested in "rebasing" the history at
>>> all, but is instead running a check on each and every commit.
>>> That is, a more expensive version of:
>>>
>>> for commit in $(git rev-list bottom..top)
>>> do
>>> git reset --hard "$commit" &&
>>> do the exec command || break
>>> done
>>>
>>> that just happens to be shorter to type?
>>> Sure, that's exactly right.
> I think I must be missing something; I had assumed that someone running
> a check on each commit wanted to ensure it passed, and, if not, they
> would want to fix the problem that caused the check to fail. In that
> case I would expect that they would want to amend the failing commit,
> rather than abort the rebase. What's the scenario where one wants to
> check a bunch of commits but not fix the ones that fail?
I suspect that is what makes this user "not interested in
'rebasing'". If finding a single breakage in the history is all
they want to accomplish (perhaps because they merely file a bug
report and make it someone else's job to fix it), they have no
desire to amend and continue the run to find further problems in
subsequent commits.
Which, of course, makes me even less sympathetic to the proposed
change.
Thanks.
^ permalink raw reply
* Re: [GSoC Patch v2 6/7] repo: add path.grafts with absolute and relative suffix formatting
From: Junio C Hamano @ 2026-07-20 4:01 UTC (permalink / raw)
To: Lucas Seiki Oshiro; +Cc: K Jayatheerth, git, jltobler
In-Reply-To: <DB49CF15-4980-4213-8463-4C0FE2EC8438@gmail.com>
Lucas Seiki Oshiro <lucasseikioshiro@gmail.com> writes:
>> Introduce `path.grafts.absolute` and `path.grafts.relative` keys to
>> `git repo info`. This allows scripting layers to query the active grafts
>> context cleanly while scaling transparently with active `GIT_GRAFT_FILE`
>> environment variable overrides.
>
> I ran `git repo info path.grafts.relative` in a repository with no
> `grafts` file, and it returned `.git/info/grafts`, which obviously
> doesn't exist.
>
> Wouldn't it be better if we check if that file exists before
> returning this value?
That is an interesting question, but I think it depends on who is
querying and for what purpose.
If a script is asking where to write the file, then the author wants
to know where the file is supposed to be, even if no such file
exists yet. Since the file format is public, they are free to write
their own tools to manipulate it.
Thanks.
^ permalink raw reply
* import-zips
From: Chris Packham @ 2026-07-20 4:31 UTC (permalink / raw)
To: GIT
Hi,
I found myself in need of a git repository that was built up from zip
files supplied by a 3rd party. I knew about
contrib/fast-import/import-zips.py but saw that it hadn't had a lot of
attention in 13 or so years.
I spend a bit of (AI assisted) time on updating things to python3. The
result is https://github.com/cpackham/import-zips.
I started the repository with a filter-repo of the existing
contrib/fast-import/import-zips.py which does lead me to one question
- what license applies? In the absence of anything else I've taken the
COPYING file from git.git i.e. GPL-2.0
I wasn't sure if there would be any interest in taking the changes
back to git.git/contrib (or if the use of AI would rule that out).
Anyway it's there on my github page if anyone else needs it. If
someone wants me to turn the changes into a series for git.git I'm
happy to do that too.
Regards,
Chris
^ permalink raw reply
* Re: [PATCH] userdiff: add support for Swift
From: Junio C Hamano @ 2026-07-20 4:55 UTC (permalink / raw)
To: Johannes Sixt
Cc: Shlok Kulshreshtha, D. Ben Knoble, René Scharfe,
Eric Sunshine, Scott L. Burson, git
In-Reply-To: <2a3a73c5-5e90-44a3-bf6a-6e98ce5e5a59@kdbg.org>
Johannes Sixt <j6t@kdbg.org> writes:
> Am 17.07.26 um 16:02 schrieb Shlok Kulshreshtha:
>> Add a built-in userdiff driver for the Swift programming language so that
>> diff hunk headers and word diffs work out of the box for ".swift" files.
>>
>> The funcname pattern is built for Swift's own declaration grammar: an
>> optional run of attributes ("@objc", "@available(iOS 13, *)", ...),
>> followed by an optional run of lowercase modifiers ("public", "static",
>> "final", ...), followed by a declaration keyword (func, class, struct,
>> enum, protocol, extension, actor, init, deinit, subscript). The keyword
>> is followed by a boundary that allows whitespace, "(" (init/subscript),
>> "?" or "!" (failable init), or "<" (generics), while still acting as a
>> word boundary so e.g. "initialize(" does not match.
>>
>> The word regex recognizes Swift identifiers, hexadecimal, octal, binary,
>> integer and floating-point literals, and the language's operators.
>>
>> Signed-off-by: Shlok Kulshreshtha <diy2903@gmail.com>
>> ---
>> This addresses the "add a userdiff driver for a language" microproject.
>
> I am mildly surprised that userdiff drivers can count as microproject.
> At a minimum, they are on the challenging side of the spectrum.
I am, too ;-) It is hard to get them right, even though thanks to
your earlier work long time ago, writing a test that demonstrates
what is expected of the patterns is fairly easy to write.
^ permalink raw reply
* Re: [PATCH 05/11] last-modified: handle repo_parse_commit() failures
From: Junio C Hamano @ 2026-07-20 5:09 UTC (permalink / raw)
To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <xmqqldbdqciy.fsf@gitster.g>
Junio C Hamano <gitster@pobox.com> writes:
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> ...
>> - repo_parse_commit(lm->rev.repo, c);
>> + if (repo_parse_commit(lm->rev.repo, c))
>> + continue;
>
> Shouldn't this be
>
> goto cleanup;
>
> instead? 'n' pulled out of not_queue may be unparseable and when we
> ignore it, don't we still want to clean up the active_paths slab for
> commit 'c'?
--- >8 ---
Subject: [PATCH] fixup! last-modified: handle repo_parse_commit() failures
https://lore.kernel.org/git/xmqqldbdqciy.fsf@gitster.g/
'n' pulled out of not_queue may be unparseable and when we ignore
it, we still want to clean up the active_paths slab for commit 'c'.
diff --git a/builtin/last-modified.c b/builtin/last-modified.c
index fe012b0c2e..3846244dfc 100644
--- a/builtin/last-modified.c
+++ b/builtin/last-modified.c
@@ -416,7 +416,7 @@ static int last_modified_run(struct last_modified *lm)
* in the '--not' queue.
*/
if (repo_parse_commit(lm->rev.repo, c))
- continue;
+ goto cleanup;
while ((n = prio_queue_get(¬_queue))) {
struct commit_list *np;
^ permalink raw reply related
* Re: [PATCH 09/11] bisect: check strbuf_getline_lf return when reading terms
From: Junio C Hamano @ 2026-07-20 5:09 UTC (permalink / raw)
To: Johannes Schindelin via GitGitGadget; +Cc: git, Johannes Schindelin
In-Reply-To: <xmqqh5m1qcfh.fsf@gitster.g>
Junio C Hamano <gitster@pobox.com> writes:
> "Johannes Schindelin via GitGitGadget" <gitgitgadget@gmail.com>
> writes:
> ...
>> diff --git a/builtin/bisect.c b/builtin/bisect.c
>> index 798e28f501..fe66d84382 100644
>> --- a/builtin/bisect.c
>> +++ b/builtin/bisect.c
>> @@ -498,9 +498,15 @@ static int get_terms(struct bisect_terms *terms)
>> }
>>
>> free_terms(terms);
>> - strbuf_getline_lf(&str, fp);
>> + if (strbuf_getline_lf(&str, fp) == EOF) {
>> + res = -1;
>> + goto finish;
>> + }
>> terms->term_bad = strbuf_detach(&str, NULL);
>> - strbuf_getline_lf(&str, fp);
>> + if (strbuf_getline_lf(&str, fp) == EOF) {
>> + res = -1;
>> + goto finish;
>> + }
>
> We want to clean-up terms->term_bad when we fail to read the second
> line after reading the first line successfully, no?
>
>> terms->term_good = strbuf_detach(&str, NULL);
>>
>> finish:
--- >8 ---
Subject: [PATCH] fixup! bisect: check strbuf_getline_lf return when reading
terms
https://lore.kernel.org/git/xmqqh5m1qcfh.fsf@gitster.g/
This fixes the immediate leak introduced by
https://lore.kernel.org/git/17c382fdf46eada79ce03a7604dd7e0454d8bea4.1784069325.git.gitgitgadget@gmail.com/
but many callers of get_terms() should all be fixed to check for
return value. If it fails to grab the replacement word for "bad",
both terms->term_bad and terms->term_good are left NULL, since the
function calls free_terms() early.
diff --git a/builtin/bisect.c b/builtin/bisect.c
index fe66d84382..69ab7ea248 100644
--- a/builtin/bisect.c
+++ b/builtin/bisect.c
@@ -505,6 +505,7 @@ static int get_terms(struct bisect_terms *terms)
terms->term_bad = strbuf_detach(&str, NULL);
if (strbuf_getline_lf(&str, fp) == EOF) {
res = -1;
+ FREE_AND_NULL(terms->term_bad);
goto finish;
}
terms->term_good = strbuf_detach(&str, NULL);
^ permalink raw reply related
* [PATCH v10 0/5] history: add squash subcommand to fold a range
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:26 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren
In-Reply-To: <pull.2337.v9.git.git.1784128573.gitgitgadget@gmail.com>
Adds git history squash <revision-range> to fold a range of commits.
Changes in v10:
* Record the full revision expression in squash reflog.
* Preserve the boundary-walk invariant when sanitizing rev-list options.
* Clarify amend! and --reedit-message documentation.
Changes in v9:
* Use the last amend! targeting the oldest folded commit as the default
squashed message. Ignore amend! markers targeting later commits while
selecting that replacement message.
* Improve tests.
Changes in v8:
* --reedit-message now builds the same editor template as git rebase -i
--autosquash: fixup!, squash! and amend! commits are grouped under the
commit they target instead of shown in commit order, and an amend!
replaces its target's message.
* A fixup!, squash! or amend! is refused only when its target is outside
the range, so several fixups for an in-range commit fold together. A
range that is entirely markers for one below-range target is combined
into a single commit, keeping the last amend! message.
* Merges inside the range are folded when the range has a single base, with
no dedicated opt-in flag, --ancestry-path ensures only commits descended
from the base are folded, and a range reaching more than one base is
rejected.
* Rev-list options are accepted and sanitized the way git replay does,
forcing the walk order back with a warning, which also fixes git history
squash -- --reverse slipping past the previous option check.
* Kept this as an explicit squash subcommand rather than making
--reedit-message the default or renaming the command.
Changes in v7:
* --reedit-message now builds the same editor template git rebase -i shows
for a squash (a combination of N commits banner with each folded message
under its own header) and follows autosquash for markers: a fixup!
message falls out (commented under a will be skipped header), while a
squash! or amend! keeps its body with only the marker subject commented
so its remark can be reworded in. Only the message text is affected,
every commit's changes are always folded in.
* Reuse git rebase -i's squash-message code: a preparatory sequencer:
commit extracts the banner, header and marker-comment helpers so both
rebase and git history squash build the identical template from one
source.
* Refuse a range whose oldest commit is a fixup!, squash! or amend!, since
the marker's target cannot be inside the range.
* Reorder the squash usage so dashed options come before <revision-range>,
and spell out HEAD instead of @ in the documentation and examples.
* Expand the squash commit message and documentation with this overview,
and scope the merge limitation so it no longer contradicts squash folding
a single-base interior merge.
Changes in v6:
* git history squash now accepts multiple revision arguments, read like the
arguments to git-rev-list, so a compound range such as @~3.. ^topic
works.
* The base to reparent onto is now the oldest in-range commit's parent; a
boundary other than that base means the range has more than one base and
is rejected. This also fixes the earlier overly-restrictive handling of
merges and side branches.
* A single-commit range (e.g. @^!) is rejected with "nothing to squash"
(this also covers the @^!-style example that previously succeeded
silently).
* Commit messages reworded: the squash commit now gives an overview of
fixup!/squash!/amend! handling, rewording, merge-parent and ref behavior.
Changes in v5:
* The range walk now uses --ancestry-path, so only commits descended from
the base are folded; a single revision such as HEAD or HEAD~1 is now
rejected as "not a <base>..<tip> range" rather than treated as a squash
down to the root.
* This adopts the --ancestry-path suggestion; the multi-base rejection is
unchanged, so a side branch that forked before the base and merged in is
still refused.
* Added tests covering more merge topologies: two interior merges, a nested
merge, an octopus merge, an octopus arm forked before the base, a merge
among the descendants replayed above the range, and a ref pointing at an
interior merge commit.
Changes in v4:
* git history squash now detects when another ref points at a commit inside
the range being folded and refuses, with an advice.historyUpdateRefs hint
to use --update-refs=head.
* A merge inside the range is folded fine as long as the range has a single
base; a range with merge commit at the tip or base also folds correctly.
Only a range with more than one base is rejected.
Changes in v3:
* Moved the feature out of git rebase and into a new git history squash
<revision-range> subcommand, per the list discussion. git rebase --squash
is dropped.
* Takes an arbitrary range (git history squash @~3.., git history squash
@~5..@~2), folding it into the oldest commit and replaying any
descendants on top.
* Implemented as a single tree operation rather than picking each commit,
so there are no repeated conflict stops (addresses Phillip's efficiency
point).
* A merge inside the range is folded fine, only a range with more than one
base is rejected.
* --reedit-message seeds the editor with every folded-in message, not just
the oldest.
Harald Nordgren (5):
history: extract helper for a commit's parent tree
history: give commit_tree_ext a message template
history: add squash subcommand to fold a range
sequencer: share the squash message marker helpers and flags
history: re-edit a squash with every message
Documentation/config/advice.adoc | 4 +
Documentation/git-history.adoc | 55 ++-
advice.c | 1 +
advice.h | 1 +
builtin/history.c | 552 ++++++++++++++++++++--
sequencer.c | 70 +--
sequencer.h | 30 ++
t/meson.build | 1 +
t/t3455-history-squash.sh | 770 +++++++++++++++++++++++++++++++
9 files changed, 1412 insertions(+), 72 deletions(-)
create mode 100755 t/t3455-history-squash.sh
base-commit: 41365c2a9ba347870b80881c0d67454edd22fd49
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2337%2FHaraldNordgren%2Frebase-fixup-fold-v10
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2337/HaraldNordgren/rebase-fixup-fold-v10
Pull-Request: https://github.com/git/git/pull/2337
Range-diff vs v9:
1: 352c818c29 = 1: f848103497 history: extract helper for a commit's parent tree
2: e06e49095b = 2: 49dadc3410 history: give commit_tree_ext a message template
3: ead974c317 ! 3: 6b5b2c93f2 history: add squash subcommand to fold a range
@@ Documentation/config/advice.adoc: all advice messages.
set as executable.
## Documentation/git-history.adoc ##
-@@ Documentation/git-history.adoc: SYNOPSIS
+@@ Documentation/git-history.adoc: git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(
git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
@@ Documentation/git-history.adoc: linkgit:gitglossary[7].
++
+A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
+targets is also in the range, so the fold does not silently absorb a
-+marker meant for a commit outside it. The body after an `amend!` subject
-+replaces the oldest commit's message when the marker targets that commit. As
-+an exception, a range made up entirely of markers for one target is combined
-+into a single commit, keeping the last `amend!` message if there is one.
++marker meant for a commit outside it. As an exception, a range made up entirely
++of markers for one target is combined into a single commit, keeping the last
++`amend!` message if there is one.
++
+A branch or tag that points at a commit inside the range would be left
+dangling once those commits are folded away, so with the default
@@ Documentation/git-history.adoc: OPTIONS
`--reedit-message`::
- Open an editor to modify the target commit's message.
-+ Open an editor to modify the rewritten commit's message. For `squash`
-+ the editor is pre-filled with the messages of all the folded commits.
++ Open an editor to modify the rewritten commit's message.
`--empty=(drop|keep|abort)`::
Control what happens when a commit becomes empty as a result of the
@@ builtin/history.c: out:
+
+ if (revs.reverse != 1 || revs.topo_order != 1 ||
+ revs.sort_order != REV_SORT_IN_GRAPH_ORDER ||
-+ revs.simplify_history != 0) {
++ revs.simplify_history != 0 || revs.boundary != 1) {
+ warning(_("ignoring rev-list options that would change how the "
+ "range is walked"));
+ revs.reverse = 1;
+ revs.topo_order = 1;
+ revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+ revs.simplify_history = 0;
++ revs.boundary = 1;
+ }
+
+ /*
@@ builtin/history.c: out:
+ goto out;
+ }
+
-+ strbuf_addf(&reflog_msg, "squash: updating %s", argv[0]);
++ strbuf_addstr(&reflog_msg, "squash: updating ");
++ strbuf_join_argv(&reflog_msg, argc, argv, ' ');
+
+ ret = handle_reference_updates(&revs, action, tip, rewritten,
+ reflog_msg.buf, dry_run,
@@ builtin/history.c: out:
+ return ret;
+}
+
- int cmd_history(int argc,
- const char **argv,
- const char *prefix,
+ static int update_worktree(struct repository *repo,
+ const struct commit *old_head,
+ const struct commit *new_head,
@@ builtin/history.c: int cmd_history(int argc,
GIT_HISTORY_FIXUP_USAGE,
GIT_HISTORY_REWORD_USAGE,
@@ builtin/history.c: int cmd_history(int argc,
## t/meson.build ##
@@ t/meson.build: integration_tests = [
- 't3451-history-reword.sh',
't3452-history-split.sh',
't3453-history-fixup.sh',
+ 't3454-history-drop.sh',
+ 't3455-history-squash.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
@@ t/t3455-history-squash.sh (new)
+
+ git history squash start..HEAD ^keep &&
+
++ git reflog -1 --format=%gs >actual &&
++ echo "squash: updating start..HEAD ^keep" >expect &&
++ test_cmp expect actual &&
++
+ check_log_subjects start..HEAD <<-\EOF &&
+ two
+ one
4: 08915cee51 = 4: 41156c9afb sequencer: share the squash message marker helpers and flags
5: fb76afe31c ! 5: cdbc183428 history: re-edit a squash with every message
@@ Documentation/git-history.adoc: already on `topic`. Rev-list options may also be
+
A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
targets is also in the range, so the fold does not silently absorb a
-@@ Documentation/git-history.adoc: marker meant for a commit outside it. The body after an `amend!` subject
- replaces the oldest commit's message when the marker targets that commit. As
- an exception, a range made up entirely of markers for one target is combined
- into a single commit, keeping the last `amend!` message if there is one.
-+The changes from every commit in the range are always folded in. Only the
-+message text differs.
+@@ Documentation/git-history.adoc: marker meant for a commit outside it. As an exception, a range made up entirely
+ of markers for one target is combined into a single commit, keeping the last
+ `amend!` message if there is one.
+ +
+With `--reedit-message` the template mirrors `git rebase -i --autosquash`:
+each `fixup!`, `squash!`, or `amend!` is grouped under the commit it
+targets rather than shown in commit order. A `fixup!` message is dropped
@@ Documentation/git-history.adoc: marker meant for a commit outside it. The body a
+subject commented, and an `amend!` replaces its target's message, unless
+a `squash!` folded into that target first, in which case it keeps its
+body like a `squash!`.
- +
+++
A branch or tag that points at a commit inside the range would be left
dangling once those commits are folded away, so with the default
+ `--update-refs=branches` the command refuses. Rerun with
+@@ Documentation/git-history.adoc: OPTIONS
+ ref updates is generally safe.
+
+ `--reedit-message`::
+- Open an editor to modify the rewritten commit's message.
++ Open an editor to modify the rewritten commit's message. For `squash`
++ the editor is pre-filled with the messages of all the folded commits.
+
+ `--empty=(drop|keep|abort)`::
+ Control what happens when a commit becomes empty as a result of the
## builtin/history.c ##
@@ builtin/history.c: static int find_interior_ref(const struct reference *ref, void *cb_data)
--
gitgitgadget
^ permalink raw reply
* [PATCH v10 1/5] history: extract helper for a commit's parent tree
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:27 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v10.git.git.1784536024.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Three places resolve the tree of a commit's first parent, falling back
to the empty tree for a root commit, each repeating the same parse and
oidcpy dance. Extract a first_parent_tree_oid() helper and route the
existing callers through it.
No change in behavior.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/history.c | 58 +++++++++++++++++++++--------------------------
1 file changed, 26 insertions(+), 32 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index d28c1f08bb..673744a55a 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -164,6 +164,25 @@ out:
return ret;
}
+static int first_parent_tree_oid(struct repository *repo,
+ struct commit *commit,
+ struct object_id *out)
+{
+ struct commit *parent = commit->parents ? commit->parents->item : NULL;
+
+ if (!parent) {
+ oidcpy(out, repo->hash_algo->empty_tree);
+ return 0;
+ }
+
+ if (repo_parse_commit(repo, parent))
+ return error(_("unable to parse parent commit %s"),
+ oid_to_hex(&parent->object.oid));
+
+ oidcpy(out, &repo_get_commit_tree(repo, parent)->object.oid);
+ return 0;
+}
+
static int commit_tree_with_edited_message(struct repository *repo,
const char *action,
struct commit *original,
@@ -171,21 +190,11 @@ static int commit_tree_with_edited_message(struct repository *repo,
{
struct object_id parent_tree_oid;
const struct object_id *tree_oid;
- struct commit *parent;
tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
- parent = original->parents ? original->parents->item : NULL;
- if (parent) {
- if (repo_parse_commit(repo, parent)) {
- return error(_("unable to parse parent commit %s"),
- oid_to_hex(&parent->object.oid));
- }
-
- parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
- } else {
- oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
- }
+ if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
+ return -1;
return commit_tree_ext(repo, action, original, original->parents,
&parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
@@ -475,18 +484,10 @@ static int commit_became_empty(struct repository *repo,
struct commit *original,
struct tree *result)
{
- struct commit *parent = original->parents ? original->parents->item : NULL;
struct object_id parent_tree_oid;
- if (parent) {
- if (repo_parse_commit(repo, parent))
- return error(_("unable to parse parent of %s"),
- oid_to_hex(&original->object.oid));
-
- parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
- } else {
- oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
- }
+ if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
+ return -1;
return oideq(&result->object.oid, &parent_tree_oid);
}
@@ -830,16 +831,9 @@ static int split_commit(struct repository *repo,
struct tree *split_tree;
int ret;
- if (original->parents) {
- if (repo_parse_commit(repo, original->parents->item)) {
- ret = error(_("unable to parse parent commit %s"),
- oid_to_hex(&original->parents->item->object.oid));
- goto out;
- }
-
- parent_tree_oid = *get_commit_tree_oid(original->parents->item);
- } else {
- oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) {
+ ret = -1;
+ goto out;
}
original_commit_tree_oid = get_commit_tree_oid(original);
--
gitgitgadget
^ permalink raw reply related
* [PATCH v10 2/5] history: give commit_tree_ext a message template
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:27 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v10.git.git.1784536024.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
commit_tree_ext() reuses the message of the commit it is handed. A
caller that folds several commits together wants to seed the message
from more than that single commit, so add an optional message_template
parameter. When NULL, the behavior is unchanged.
Pass NULL from the existing fixup and split callers.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
builtin/history.c | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 673744a55a..b592b98393 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -108,6 +108,7 @@ enum commit_tree_flags {
static int commit_tree_ext(struct repository *repo,
const char *action,
struct commit *commit_with_message,
+ const char *message_template,
const struct commit_list *parents,
const struct object_id *old_tree,
const struct object_id *new_tree,
@@ -137,13 +138,16 @@ static int commit_tree_ext(struct repository *repo,
original_author = xmemdupz(ptr, len);
find_commit_subject(original_message, &original_body);
+ if (!message_template)
+ message_template = original_body;
+
if (flags & COMMIT_TREE_EDIT_MESSAGE) {
ret = fill_commit_message(repo, old_tree, new_tree,
- original_body, action, &commit_message);
+ message_template, action, &commit_message);
if (ret < 0)
goto out;
} else {
- strbuf_addstr(&commit_message, original_body);
+ strbuf_addstr(&commit_message, message_template);
}
original_extra_headers = read_commit_extra_headers(commit_with_message,
@@ -196,7 +200,7 @@ static int commit_tree_with_edited_message(struct repository *repo,
if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
return -1;
- return commit_tree_ext(repo, action, original, original->parents,
+ return commit_tree_ext(repo, action, original, NULL, original->parents,
&parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
}
@@ -675,7 +679,7 @@ static int cmd_history_fixup(int argc,
goto out;
if (!skip_commit) {
- ret = commit_tree_ext(repo, "fixup", original, original->parents,
+ ret = commit_tree_ext(repo, "fixup", original, NULL, original->parents,
&original_tree->object.oid, &merge_result.tree->object.oid,
&rewritten, flags);
if (ret < 0) {
@@ -886,7 +890,7 @@ static int split_commit(struct repository *repo,
* The first commit is constructed from the split-out tree. The base
* that shall be diffed against is the parent of the original commit.
*/
- ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
+ ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid,
&split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing first commit"));
@@ -903,7 +907,7 @@ static int split_commit(struct repository *repo,
old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
- ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
+ ret = commit_tree_ext(repo, "split-out", original, NULL, parents, old_tree_oid,
new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing second commit"));
--
gitgitgadget
^ permalink raw reply related
* [PATCH v10 3/5] history: add squash subcommand to fold a range
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:27 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v10.git.git.1784536024.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
Folding a series of commits into one required either an interactive
rebase where each commit after the first was hand-edited to "fixup", or
a "git reset --soft" to the merge base followed by "git commit --amend".
Add "git history squash <revision-range>" to do this directly. It folds
every commit in the range into the oldest one, keeping that commit's
authorship and taking the tree of the newest commit, then replays the
commits above the range on top. The squashed message comes from the
oldest commit by default, or from the body of the last amend! commit
targeting it. An editor opens with the selected message when
--reedit-message is given. A fixup!, squash! or amend! commit is refused
unless the commit it targets is also in the range, so the fold does not
silently absorb a marker meant for a commit outside it. The check runs
the range through todo_list_rearrange_squash(), which leaves such a
marker as a plain pick. Markers whose target is in the range fold in as
usual. As an exception, a range made up entirely of markers for one
target is combined anyway, taking its message from the last amend! if
there is one, so a batch of fixups for the same commit can be collapsed.
The range is read like the arguments to "git rev-list", so several
revisions such as "HEAD~3..HEAD ^topic" may be given, and rev-list
options are accepted too. As "git replay" does, the walk options the fold
relies on are forced after setup_revisions() and a warning is printed if
an option changed them, so the first commit returned is the range's
oldest and its parent is the base regardless of what the user passed
(including after a "--"). A merge inside the range is folded when its
other parent is reachable from the base, otherwise the range has more
than one base and is rejected. By default the command also refuses when a
ref points at a commit that the fold would discard. Use --update-refs=head
to rewrite only the current branch instead.
Inspired-by: Sergey Chernov <serega.morph@gmail.com>
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/config/advice.adoc | 4 +
Documentation/git-history.adoc | 45 ++-
advice.c | 1 +
advice.h | 1 +
builtin/history.c | 374 ++++++++++++++++++++
t/meson.build | 1 +
t/t3455-history-squash.sh | 569 +++++++++++++++++++++++++++++++
7 files changed, 992 insertions(+), 3 deletions(-)
create mode 100755 t/t3455-history-squash.sh
diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc
index 81f80a9274..e2a3487778 100644
--- a/Documentation/config/advice.adoc
+++ b/Documentation/config/advice.adoc
@@ -59,6 +59,10 @@ all advice messages.
forceDeleteBranch::
Shown when the user tries to delete a not fully merged
branch without the force option set.
+ historyUpdateRefs::
+ Shown when `git history squash` refuses because a ref points
+ into the range being folded, to tell the user about
+ `--update-refs=head`.
ignoredHook::
Shown when a hook is ignored because the hook is not
set as executable.
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 28b477cd37..e1e930f355 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -12,6 +12,7 @@ git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(
git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
+git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] <revision-range>
DESCRIPTION
-----------
@@ -43,8 +44,11 @@ at once.
LIMITATIONS
-----------
-This command does not (yet) work with histories that contain merges. You
-should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.
+This command does not (yet) replay merge commits onto the rewritten
+history: if a commit that would be replayed is a merge, the operation is
+rejected, and you should use linkgit:git-rebase[1] with the
+`--rebase-merges` flag instead. The `squash` subcommand can still fold a
+merge that lies inside the range, as long as the range has a single base.
Furthermore, the command does not support operations that can result in merge
conflicts. This limitation is by design as history rewrites are not intended to
@@ -113,6 +117,41 @@ linkgit:gitglossary[7].
It is invalid to select either all or no hunks, as that would lead to
one of the commits becoming empty.
+`squash <revision-range>`::
+ Fold all commits in _<revision-range>_ into the oldest commit of that
+ range. The resulting commit keeps the oldest commit's authorship and
+ takes the tree of the range's newest commit, so the whole range
+ collapses into a single commit. Commits above the range are replayed
+ on top of the result.
++
+The range is given in the usual `<base>..<tip>` form, where _<base>_ is
+the commit just below the oldest commit to squash. For example, `git
+history squash HEAD~3..HEAD` folds the three most recent commits into
+one, and `git history squash HEAD~5..HEAD~2` squashes an interior range
+while leaving the two newest commits in place. Several revisions may be
+given, for example `HEAD~3..HEAD ^topic` to additionally exclude what is
+already on `topic`. Rev-list options may also be given, but any that would
+change how the range is walked are overridden with a warning.
++
+The oldest commit's message is preserved by default, except that an `amend!`
+commit targeting it replaces its message. Specify `--reedit-message` to edit
+the resulting message. A merge commit inside the range is folded like any
+other, but the range must have a single base, so a range that reaches more
+than one entry point (for example a side branch that forked before the range
+and was later merged into it) is rejected.
++
+A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
+targets is also in the range, so the fold does not silently absorb a
+marker meant for a commit outside it. As an exception, a range made up entirely
+of markers for one target is combined into a single commit, keeping the last
+`amend!` message if there is one.
++
+A branch or tag that points at a commit inside the range would be left
+dangling once those commits are folded away, so with the default
+`--update-refs=branches` the command refuses. Rerun with
+`--update-refs=head` to rewrite only the current branch and leave such
+refs pointing at the old commits.
+
OPTIONS
-------
@@ -123,7 +162,7 @@ OPTIONS
ref updates is generally safe.
`--reedit-message`::
- Open an editor to modify the target commit's message.
+ Open an editor to modify the rewritten commit's message.
`--empty=(drop|keep|abort)`::
Control what happens when a commit becomes empty as a result of the
diff --git a/advice.c b/advice.c
index 63bf8b0c5f..401d047391 100644
--- a/advice.c
+++ b/advice.c
@@ -58,6 +58,7 @@ static struct {
[ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
[ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
[ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },
+ [ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" },
[ADVICE_IGNORED_HOOK] = { "ignoredHook" },
[ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" },
[ADVICE_MERGE_CONFLICT] = { "mergeConflict" },
diff --git a/advice.h b/advice.h
index 66f6cd6a77..3f0b4f0485 100644
--- a/advice.h
+++ b/advice.h
@@ -25,6 +25,7 @@ enum advice_type {
ADVICE_FETCH_SHOW_FORCED_UPDATES,
ADVICE_FORCE_DELETE_BRANCH,
ADVICE_GRAFT_FILE_DEPRECATED,
+ ADVICE_HISTORY_UPDATE_REFS,
ADVICE_IGNORED_HOOK,
ADVICE_IMPLICIT_IDENTITY,
ADVICE_MERGE_CONFLICT,
diff --git a/builtin/history.c b/builtin/history.c
index b592b98393..423c8beaaf 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "advice.h"
#include "cache-tree.h"
#include "commit.h"
#include "commit-reach.h"
@@ -34,6 +35,8 @@
N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
#define GIT_HISTORY_SPLIT_USAGE \
N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
+#define GIT_HISTORY_SQUASH_USAGE \
+ N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--reedit-message] <revision-range>")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -1004,6 +1007,375 @@ out:
return ret;
}
+/*
+ * Resolve a "<base>..<tip>" revision range into the base commit just outside
+ * the range (which becomes the parent of the squashed commit), the oldest
+ * commit contained in the range (whose message the squash reuses), and the
+ * range tip (whose tree becomes the result). A merge inside the range is fine,
+ * but the range must have a single base and must not reach a root commit.
+ */
+static int resolve_squash_range(struct repository *repo,
+ const char **argv,
+ struct commit **base_out,
+ struct commit **oldest_out,
+ struct commit **tip_out,
+ struct oidset *interior_out)
+{
+ struct rev_info revs;
+ struct commit *commit, *base = NULL, *oldest = NULL, *tip = NULL;
+ struct commit_list *boundaries = NULL, *b;
+ struct strvec args = STRVEC_INIT;
+ size_t i;
+ int ret;
+
+ repo_init_revisions(repo, &revs, NULL);
+ revs.reverse = 1;
+ revs.topo_order = 1;
+ revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+ revs.simplify_history = 0;
+ revs.boundary = 1;
+
+ strvec_push(&args, "ignored");
+ strvec_push(&args, "--ancestry-path");
+ strvec_pushv(&args, argv);
+ setup_revisions_from_strvec(&args, &revs, NULL);
+ if (args.nr != 1) {
+ ret = error(_("unrecognized argument: %s"), args.v[1]);
+ goto out;
+ }
+
+ if (revs.reverse != 1 || revs.topo_order != 1 ||
+ revs.sort_order != REV_SORT_IN_GRAPH_ORDER ||
+ revs.simplify_history != 0 || revs.boundary != 1) {
+ warning(_("ignoring rev-list options that would change how the "
+ "range is walked"));
+ revs.reverse = 1;
+ revs.topo_order = 1;
+ revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
+ revs.simplify_history = 0;
+ revs.boundary = 1;
+ }
+
+ /*
+ * A squash needs a base to reparent onto, so the range has to exclude
+ * something, as in "<base>..<tip>". A revision range with no such
+ * bottom commit cannot be squashed.
+ */
+ for (i = 0; i < revs.cmdline.nr; i++)
+ if (revs.cmdline.rev[i].flags & UNINTERESTING)
+ break;
+ if (i == revs.cmdline.nr) {
+ ret = error(_("not a '<base>..<tip>' revision range"));
+ goto out;
+ }
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ /*
+ * Set boundary commits aside for the base check below, and put every
+ * in-range commit but the tip into the interior set. A ref pointing
+ * at an interior commit would dangle once the range is folded away.
+ */
+ while ((commit = get_revision(&revs))) {
+ if (commit->object.flags & BOUNDARY) {
+ commit_list_insert(commit, &boundaries);
+ continue;
+ }
+ if (!oldest)
+ oldest = commit;
+ if (tip)
+ oidset_insert(interior_out, &tip->object.oid);
+ tip = commit;
+ }
+
+ if (!oldest) {
+ ret = error(_("the revision range is empty"));
+ goto out;
+ } else if (oldest == tip) {
+ ret = error(_("the revision range holds a single commit; "
+ "nothing to squash"));
+ goto out;
+ } else if (!oldest->parents) {
+ BUG("an in-range commit must have a parent");
+ }
+ base = oldest->parents->item;
+
+ /*
+ * A boundary other than the base is an in-range commit reaching a
+ * commit outside the range, so the range has more than one base.
+ */
+ for (b = boundaries; b; b = b->next) {
+ if (b->item != base) {
+ ret = error(_("the revision range has more than one base; "
+ "cannot squash"));
+ goto out;
+ }
+ }
+
+ *base_out = base;
+ *oldest_out = oldest;
+ *tip_out = tip;
+ ret = 0;
+
+out:
+ commit_list_free(boundaries);
+ reset_revision_walk();
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
+static const char *autosquash_target(const char *subject)
+{
+ const char *rest;
+
+ while (skip_prefix(subject, "fixup! ", &rest) ||
+ skip_prefix(subject, "squash! ", &rest) ||
+ skip_prefix(subject, "amend! ", &rest))
+ subject = rest;
+ return subject;
+}
+
+static int reject_dangling_fixups(struct repository *repo,
+ struct commit *base,
+ struct commit *tip,
+ struct commit *oldest,
+ struct commit **msg_source,
+ struct commit **amend_source)
+{
+ struct todo_list todo = TODO_LIST_INIT;
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ struct rev_info revs;
+ struct commit *commit, *last_amend = NULL;
+ struct strvec args = STRVEC_INIT;
+ char *dangling_subject = NULL, *dangling_target = NULL;
+ bool mixed_target = false, all_fixups_one_target;
+ bool past_oldest_group = false;
+ int i, ret, nr_dangling = 0;
+
+ *msg_source = oldest;
+ *amend_source = NULL;
+
+ repo_init_revisions(repo, &revs, NULL);
+ strvec_push(&args, "ignored");
+ strvec_push(&args, "--reverse");
+ strvec_push(&args, "--topo-order");
+ strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
+ oid_to_hex(&tip->object.oid));
+ setup_revisions_from_strvec(&args, &revs, NULL);
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+ while ((commit = get_revision(&revs)))
+ strbuf_addf(&todo.buf, "pick %s\n",
+ oid_to_hex(&commit->object.oid));
+
+ if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
+ todo_list_rearrange_squash(&todo) < 0) {
+ ret = error(_("could not check the range for fixups"));
+ goto out;
+ }
+
+ for (i = 0; i < todo.nr; i++) {
+ const char *message, *subject_start, *target;
+ char *subject;
+ size_t sublen;
+
+ message = repo_logmsg_reencode(repo, todo.items[i].commit,
+ NULL, NULL);
+ sublen = find_commit_subject(message, &subject_start);
+
+ if (todo.items[i].command != TODO_PICK) {
+ if (!past_oldest_group &&
+ starts_with(subject_start, "amend! "))
+ *amend_source = todo.items[i].commit;
+ repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
+ continue;
+ }
+ if (i)
+ past_oldest_group = true;
+
+ subject = xmemdupz(subject_start, sublen);
+ target = autosquash_target(subject);
+ if (target != subject) {
+ nr_dangling++;
+ if (!dangling_target) {
+ dangling_target = xstrdup(target);
+ dangling_subject = xstrdup(subject);
+ } else if (strcmp(dangling_target, target)) {
+ mixed_target = true;
+ }
+ if (starts_with(subject, "amend! "))
+ last_amend = todo.items[i].commit;
+ }
+ free(subject);
+ repo_unuse_commit_buffer(repo, todo.items[i].commit, message);
+ }
+
+ all_fixups_one_target = nr_dangling == todo.nr && !mixed_target;
+ if (nr_dangling && !all_fixups_one_target) {
+ ret = error(_("cannot squash '%s': its target is not in the "
+ "range"), dangling_subject);
+ } else {
+ if (last_amend)
+ *msg_source = last_amend;
+ ret = 0;
+ }
+
+out:
+ free(dangling_subject);
+ free(dangling_target);
+ todo_list_release(&todo);
+ replay_opts_release(&opts);
+ reset_revision_walk();
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
+struct interior_ref_cb {
+ const struct oidset *interior;
+ const char *name;
+};
+
+static int find_interior_ref(const struct reference *ref, void *cb_data)
+{
+ struct interior_ref_cb *data = cb_data;
+
+ if (oidset_contains(data->interior, ref->oid)) {
+ data->name = xstrdup(ref->name);
+ return 1;
+ }
+
+ return 0;
+}
+
+static int cmd_history_squash(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_SQUASH_USAGE,
+ NULL,
+ };
+ enum ref_action action = REF_ACTION_DEFAULT;
+ enum commit_tree_flags flags = 0;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+ N_("control which refs should be updated"),
+ PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
+ OPT_BIT(0, "reedit-message", &flags,
+ N_("open an editor to modify the commit message"),
+ COMMIT_TREE_EDIT_MESSAGE),
+ OPT_END(),
+ };
+ struct strbuf reflog_msg = STRBUF_INIT;
+ struct strbuf message = STRBUF_INIT;
+ struct oidset interior = OIDSET_INIT;
+ struct commit *base, *oldest, *tip, *rewritten, *msg_source,
+ *amend_source;
+ const struct object_id *base_tree_oid, *tip_tree_oid;
+ const char *message_template = NULL;
+ struct commit_list *parents = NULL;
+ struct rev_info revs = { 0 };
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage,
+ PARSE_OPT_KEEP_UNKNOWN_OPT);
+ if (!argc) {
+ ret = error(_("command expects a revision range"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+
+ ret = resolve_squash_range(repo, argv, &base, &oldest, &tip,
+ &interior);
+ if (ret < 0)
+ goto out;
+
+ ret = reject_dangling_fixups(repo, base, tip, oldest, &msg_source,
+ &amend_source);
+ if (ret < 0)
+ goto out;
+ if (amend_source) {
+ const char *amend_message, *body;
+
+ amend_message = repo_logmsg_reencode(repo, amend_source,
+ NULL, NULL);
+ find_commit_subject(amend_message, &body);
+ body = skip_blank_lines(body + commit_subject_length(body));
+ strbuf_addstr(&message, body);
+ message_template = message.buf;
+ repo_unuse_commit_buffer(repo, amend_source, amend_message);
+ }
+
+ if (action == REF_ACTION_BRANCHES) {
+ struct interior_ref_cb cb = { .interior = &interior };
+
+ refs_for_each_ref(get_main_ref_store(repo),
+ find_interior_ref, &cb);
+ if (cb.name) {
+ ret = error(_("'%s' points into the squashed range"),
+ cb.name);
+ advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
+ _("Use --update-refs=head to rewrite only "
+ "the current branch and leave such refs "
+ "untouched."));
+ free((char *)cb.name);
+ goto out;
+ }
+ }
+
+ ret = setup_revwalk(repo, action, tip, &revs);
+ if (ret < 0)
+ goto out;
+
+ base_tree_oid = &repo_get_commit_tree(repo, base)->object.oid;
+ tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
+ commit_list_append(base, &parents);
+
+ ret = commit_tree_ext(repo, "squash", msg_source, message_template,
+ parents,
+ base_tree_oid, tip_tree_oid, &rewritten, flags);
+ if (ret < 0) {
+ ret = error(_("failed writing squashed commit"));
+ goto out;
+ }
+
+ strbuf_addstr(&reflog_msg, "squash: updating ");
+ strbuf_join_argv(&reflog_msg, argc, argv, ' ');
+
+ ret = handle_reference_updates(&revs, action, tip, rewritten,
+ reflog_msg.buf, dry_run,
+ REPLAY_EMPTY_COMMIT_ABORT);
+ if (ret < 0) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&reflog_msg);
+ strbuf_release(&message);
+ oidset_clear(&interior);
+ commit_list_free(parents);
+ release_revisions(&revs);
+ return ret;
+}
+
static int update_worktree(struct repository *repo,
const struct commit *old_head,
const struct commit *new_head,
@@ -1192,6 +1564,7 @@ int cmd_history(int argc,
GIT_HISTORY_FIXUP_USAGE,
GIT_HISTORY_REWORD_USAGE,
GIT_HISTORY_SPLIT_USAGE,
+ GIT_HISTORY_SQUASH_USAGE,
NULL,
};
parse_opt_subcommand_fn *fn = NULL;
@@ -1200,6 +1573,7 @@ int cmd_history(int argc,
OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
+ OPT_SUBCOMMAND("squash", &fn, cmd_history_squash),
OPT_END(),
};
diff --git a/t/meson.build b/t/meson.build
index 8ae6ab6c5f..89cff16405 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -405,6 +405,7 @@ integration_tests = [
't3452-history-split.sh',
't3453-history-fixup.sh',
't3454-history-drop.sh',
+ 't3455-history-squash.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh
new file mode 100755
index 0000000000..9c362f3094
--- /dev/null
+++ b/t/t3455-history-squash.sh
@@ -0,0 +1,569 @@
+#!/bin/sh
+
+test_description='tests for git-history squash subcommand'
+
+. ./test-lib.sh
+
+stage_file () {
+ printf "%s\n" "$1" >file &&
+ git add file
+}
+
+commit_with_message () {
+ printf "%b" "$1" >msg &&
+ git commit --allow-empty -qF msg
+}
+
+check_commit_count () {
+ git rev-list --count "$1" >actual &&
+ echo "$2" >expect &&
+ test_cmp expect actual
+}
+
+check_log_subjects () {
+ git log --format="%s" "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+check_log_messages () {
+ git log --format="%B" "$1" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'setup linear history touching two files' '
+ test_commit base file a &&
+ git tag start &&
+ test_commit --no-tag one other x &&
+ test_commit --no-tag two file c &&
+ test_commit three file d
+'
+
+test_expect_success 'errors on missing range argument' '
+ test_must_fail git history squash 2>err &&
+ test_grep "expects a revision range" err
+'
+
+test_expect_success 'errors on an empty range' '
+ test_must_fail git history squash HEAD..HEAD 2>err &&
+ test_grep "the revision range is empty" err
+'
+
+test_expect_success 'errors on a single revision that is not a range' '
+ test_must_fail git history squash HEAD 2>err &&
+ test_grep "not a .*range" err &&
+ test_must_fail git history squash HEAD~1 2>err &&
+ test_grep "not a .*range" err
+'
+
+test_expect_success 'errors on a range holding a single commit' '
+ git reset --hard three &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash "HEAD^!" 2>err &&
+ test_grep "single commit; nothing to squash" err &&
+ test_cmp_rev "$head_before" HEAD
+'
+
+test_expect_success 'accepts multiple revision arguments with an exclusion' '
+ git reset --hard three &&
+ git branch -f keep HEAD~2 &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start..HEAD ^keep &&
+
+ git reflog -1 --format=%gs >actual &&
+ echo "squash: updating start..HEAD ^keep" >expect &&
+ test_cmp expect actual &&
+
+ check_log_subjects start..HEAD <<-\EOF &&
+ two
+ one
+ EOF
+ test_cmp_rev keep HEAD~1 &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+
+ git branch -D keep
+'
+
+test_expect_success 'squashes a branch the current branch is not on' '
+ git reset --hard three &&
+ main=$(git symbolic-ref --short HEAD) &&
+ head_before=$(git rev-parse HEAD) &&
+ git checkout -b off-history start &&
+ test_commit --no-tag off-one off a &&
+ test_commit --no-tag off-two off b &&
+ git checkout "$main" &&
+
+ git history squash start..off-history &&
+
+ check_commit_count start..off-history 1 &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git branch -D off-history
+'
+
+test_expect_success 'squashes a range into a single commit without changing the tree' '
+ git reset --hard three &&
+ head_before=$(git rev-parse HEAD) &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash --dry-run start.. >out &&
+ predicted=$(awk "/^update refs\/heads\// {print \$3}" out) &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git history squash start.. &&
+
+ test "$predicted" = "$(git rev-parse HEAD)" &&
+ check_commit_count start..HEAD 1 &&
+ test_cmp_rev start HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ check_log_subjects -1 <<-\EOF &&
+ one
+ EOF
+ git reflog >reflog &&
+ test_grep "squash: updating" reflog
+'
+
+test_expect_success 'sanitizes rev-list walk options, before and after --' '
+ git reset --hard three &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash --date-order start.. 2>err &&
+ test_grep "ignoring rev-list options" err &&
+ test_cmp_rev start HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+
+ git reset --hard three &&
+ git history squash -- --reverse start.. 2>err &&
+ test_grep "ignoring rev-list options" err &&
+ test_cmp_rev start HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
+'
+
+test_expect_success 'squashes an interior range and replays descendants verbatim' '
+ git reset --hard three &&
+ final_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start..@~1 &&
+
+ check_log_subjects start..HEAD <<-\EOF &&
+ three
+ one
+ EOF
+
+ test_cmp_rev start HEAD~2 &&
+ test "$final_tree" = "$(git rev-parse HEAD^{tree})"
+'
+
+test_expect_success 'squashes when the base is the root commit' '
+ git reset --hard three &&
+ root=$(git rev-list --max-parents=0 HEAD) &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash "$root.." &&
+
+ check_commit_count "$root..HEAD" 1 &&
+ test_cmp_rev "$root" HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
+'
+
+
+test_expect_success 'folds fixups whose target is in the range' '
+ git reset --hard start &&
+ test_commit --no-tag target file b &&
+ git commit --allow-empty -m "fixup! target" &&
+ git commit --allow-empty -m "fixup! target" &&
+ test_commit --no-tag later file c &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ check_log_subjects -1 <<-\EOF
+ target
+ EOF
+'
+
+test_expect_success 'refuses a below-range fixup! after an in-range commit' '
+ git reset --hard start &&
+ test_commit --no-tag inside file b &&
+ test_commit --no-tag "fixup! outside" file c &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
+ test_grep "target is not in the range" err &&
+ test_cmp_rev "$head_before" HEAD
+'
+
+test_expect_success 'combines a run of fixups for one commit below the range' '
+ git reset --hard start &&
+ stage_file b && git commit -m "fixup! base" &&
+ stage_file c && git commit -m "fixup! base" &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ check_log_subjects -1 <<-\EOF
+ fixup! base
+ EOF
+'
+
+test_expect_success 'combining below-range fixups keeps the last amend! message' '
+ git reset --hard start &&
+ stage_file b && git commit -m "fixup! base" &&
+ stage_file c &&
+ commit_with_message "amend! base\n\namended body\n" &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ check_log_messages -1 <<-\EOF
+ amend! base
+
+ amended body
+
+ EOF
+'
+
+test_expect_success 'refuses fixups for two different commits below the range' '
+ git reset --hard start &&
+ stage_file b && git commit -m "fixup! aaa" &&
+ stage_file c && git commit -m "fixup! bbb" &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
+ test_grep "target is not in the range" err &&
+ test_cmp_rev "$head_before" HEAD
+'
+
+test_expect_success 'the last amend! for the oldest commit replaces its message' '
+ git reset --hard start &&
+ test_commit --no-tag marker-oldest file b &&
+ git commit --allow-empty -m "squash! marker-oldest" &&
+ commit_with_message "amend! marker-oldest\n\nearlier message\n" &&
+ commit_with_message \
+ "amend! marker-oldest\n\namended subject\n\namended body\n" &&
+ test_commit --no-tag marker-later file c &&
+ commit_with_message "amend! marker-later\n\nwrong message\n" &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ check_log_messages -1 <<-\EOF
+ amended subject
+
+ amended body
+
+ EOF
+'
+
+test_expect_success 'preserves authorship of the oldest commit' '
+ git reset --hard start &&
+ GIT_AUTHOR_NAME=Squasher GIT_AUTHOR_EMAIL=squash@example.com \
+ test_commit --no-tag oldest file b &&
+ test_commit newest file c &&
+
+ git history squash start.. &&
+
+ git log -1 --format="%an <%ae>" >actual &&
+ echo "Squasher <squash@example.com>" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--update-refs=head only moves HEAD' '
+ git reset --hard three &&
+ git branch -f other HEAD &&
+ other_before=$(git rev-parse other) &&
+
+ git history squash --update-refs=head start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test_cmp_rev "$other_before" other
+'
+
+test_expect_success 'refuses to fold a range a ref points into' '
+ git reset --hard three &&
+ git branch -f mid HEAD~1 &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
+ test_grep "error: .* points into the squashed range" err &&
+ test_grep "hint: .*--update-refs=head" err &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git branch -D mid
+'
+
+test_expect_success 'advice.historyUpdateRefs silences the hint' '
+ git reset --hard three &&
+ git branch -f mid HEAD~1 &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git -c advice.historyUpdateRefs=false \
+ history squash start.. 2>err &&
+ test_grep "points into the squashed range" err &&
+ test_grep ! "hint:" err &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git branch -D mid
+'
+
+test_expect_success '--update-refs=head folds past a ref pointing into the range' '
+ git reset --hard three &&
+ git branch -f mid HEAD~1 &&
+ mid_before=$(git rev-parse mid) &&
+
+ git history squash --update-refs=head start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test_cmp_rev "$mid_before" mid &&
+
+ git branch -D mid
+'
+
+test_expect_success 'refuses to fold a range a tag points into' '
+ git reset --hard three &&
+ git tag -f mark HEAD~1 &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
+ test_grep "refs/tags/mark" err &&
+ test_grep "points into the squashed range" err &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git tag -d mark
+'
+
+test_expect_success 'squashes a range whose internal merge has a single base' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag before-side file b &&
+ git checkout -b inner-side &&
+ test_commit --no-tag on-inner-side inner x &&
+ git checkout "$main" &&
+ test_commit --no-tag after-side file c &&
+ git merge --no-ff -m merge inner-side &&
+ git branch -D inner-side &&
+ test_commit --no-tag after-merge file d &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ check_log_subjects -1 <<-\EOF &&
+ before-side
+ EOF
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file inner
+'
+
+test_expect_success 'folds a merge of a branch that forked at the base' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ git checkout -b base-fork-side &&
+ test_commit --no-tag base-fork-side side x &&
+ git checkout "$main" &&
+ test_commit --no-tag base-fork-main file b &&
+ git merge --no-ff -m "merge base-fork-side" base-fork-side &&
+ git branch -D base-fork-side &&
+ test_commit --no-tag base-fork-tail file c &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test_cmp_rev start HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file side
+'
+
+test_expect_success 'refuses a merge whose other parent is outside the range' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ git checkout -b outside-parent &&
+ test_commit --no-tag outside-parent outside x &&
+ git checkout "$main" &&
+ test_commit --no-tag outside-main file b &&
+ base=$(git rev-parse HEAD) &&
+ test_commit --no-tag outside-mid file c &&
+ git merge --no-ff -m "merge outside-parent" outside-parent &&
+ git branch -D outside-parent &&
+ merged=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash "$base.." 2>err &&
+ test_grep "more than one base" err &&
+ test_cmp_rev "$merged" HEAD
+'
+
+test_expect_success 'folds a range whose tip is a merge commit' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag tipmerge-base file b &&
+ git checkout -b tipmerge-side &&
+ test_commit --no-tag tipmerge-side side x &&
+ git checkout "$main" &&
+ test_commit --no-tag tipmerge-main file c &&
+ git merge --no-ff -m "merge tipmerge-side" tipmerge-side &&
+ git branch -D tipmerge-side &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file side
+'
+
+test_expect_success 'folds a range whose base is a merge commit' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ git checkout -b basemerge-side &&
+ test_commit --no-tag basemerge-side side x &&
+ git checkout "$main" &&
+ test_commit --no-tag basemerge-main file b &&
+ git merge --no-ff -m "merge basemerge-side" basemerge-side &&
+ git branch -D basemerge-side &&
+ base=$(git rev-parse HEAD) &&
+ test_commit --no-tag basemerge-one file c &&
+ test_commit --no-tag basemerge-two file d &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash "$base.." &&
+
+ check_commit_count "$base..HEAD" 1 &&
+ test_cmp_rev "$base" HEAD^ &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
+'
+
+test_expect_success 'folds a range with two interior merges' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag two-merge-a file a1 &&
+ git checkout -b two-merge-s1 &&
+ test_commit --no-tag two-merge-s1 s1 x &&
+ git checkout "$main" &&
+ git merge --no-ff -m "merge s1" two-merge-s1 &&
+ test_commit --no-tag two-merge-b file b1 &&
+ git checkout -b two-merge-s2 &&
+ test_commit --no-tag two-merge-s2 s2 y &&
+ git checkout "$main" &&
+ git merge --no-ff -m "merge s2" two-merge-s2 &&
+ git branch -D two-merge-s1 two-merge-s2 &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file s1 &&
+ test_path_is_file s2
+'
+
+test_expect_success 'folds a range with a nested merge' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ git checkout -b nested-outer &&
+ test_commit --no-tag nested-outer outer x &&
+ git checkout -b nested-inner &&
+ test_commit --no-tag nested-inner inner y &&
+ git checkout nested-outer &&
+ git merge --no-ff -m "merge inner" nested-inner &&
+ git checkout "$main" &&
+ test_commit --no-tag nested-main file b1 &&
+ git merge --no-ff -m "merge outer" nested-outer &&
+ git branch -D nested-outer nested-inner &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file outer &&
+ test_path_is_file inner
+'
+
+test_expect_success 'folds a range with an octopus merge' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag octo-base file a1 &&
+ git checkout -b octo-1 &&
+ test_commit --no-tag octo-1 o1 x &&
+ git checkout "$main" &&
+ git checkout -b octo-2 &&
+ test_commit --no-tag octo-2 o2 y &&
+ git checkout "$main" &&
+ git merge --no-ff -m octopus octo-1 octo-2 &&
+ git branch -D octo-1 octo-2 &&
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
+
+ git history squash start.. &&
+
+ check_commit_count start..HEAD 1 &&
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
+ test_path_is_file o1 &&
+ test_path_is_file o2
+'
+
+test_expect_success 'refuses an octopus merge with an arm forked before the base' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ git checkout -b octo-pre &&
+ test_commit octo-pre-side pside x &&
+ git checkout "$main" &&
+ test_commit octo-pre-main file b1 &&
+ octo_base=$(git rev-parse HEAD) &&
+ git checkout -b octo-within &&
+ test_commit --no-tag octo-within wside y &&
+ git checkout "$main" &&
+ git merge --no-ff -m octopus octo-pre octo-within &&
+ merged=$(git rev-parse HEAD) &&
+ git branch -D octo-pre octo-within &&
+
+ test_must_fail git history squash "$octo_base.." 2>err &&
+ test_grep "more than one base" err &&
+ test_cmp_rev "$merged" HEAD
+'
+
+test_expect_success 'refuses when a descendant above the range is a merge' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag desc-one file b &&
+ test_commit --no-tag desc-two file c &&
+ git tag desc-tip &&
+ git checkout -b desc-above &&
+ test_commit --no-tag desc-above above x &&
+ git checkout "$main" &&
+ test_commit --no-tag desc-main file d &&
+ git merge --no-ff -m "merge desc-above" desc-above &&
+ git branch -D desc-above &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start..desc-tip 2>err &&
+ test_grep "merge commits is not supported" err &&
+ test_cmp_rev "$head_before" HEAD
+'
+
+test_expect_success 'refuses to fold a range a ref points into at a merge' '
+ git reset --hard start &&
+ main=$(git symbolic-ref --short HEAD) &&
+ test_commit --no-tag refmerge-base file b &&
+ git checkout -b refmerge-side &&
+ test_commit --no-tag refmerge-side side x &&
+ git checkout "$main" &&
+ test_commit --no-tag refmerge-main file c &&
+ git merge --no-ff -m "interior merge" refmerge-side &&
+ git branch -D refmerge-side &&
+ git branch at-merge HEAD &&
+ test_commit --no-tag refmerge-tail file d &&
+ head_before=$(git rev-parse HEAD) &&
+
+ test_must_fail git history squash start.. 2>err &&
+ test_grep "at-merge" err &&
+ test_grep "points into the squashed range" err &&
+ test_cmp_rev "$head_before" HEAD &&
+
+ git branch -D at-merge
+'
+
+test_done
--
gitgitgadget
^ permalink raw reply related
* [PATCH v10 4/5] sequencer: share the squash message marker helpers and flags
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:27 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v10.git.git.1784536024.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
When "git rebase -i" squashes commits it builds an editor template with a
"This is a combination of N commits." banner, a "This is the 1st/Nth
commit message:" header above each kept message (or a "will be skipped"
header for a dropped one), and a commented-out subject for any fixup!,
squash! or amend! commit. The banner, the headers and the
subject-commenting all live in static helpers in sequencer.c wired to the
rebase state, so no other command can present a squash the same way.
Pull the three pieces out into add_squash_combination_header(),
add_squash_message_header() (which takes a flag for the "will be skipped"
variant) and squash_subject_comment_len(), and use them from
update_squash_messages() and append_squash_message(). Also move the
todo_item_flags enum to the header, so a caller reading the output of
todo_list_rearrange_squash() can tell an amend! (TODO_REPLACE_FIXUP_MSG)
from a plain fixup!. A later change reuses all of this to give "git
history squash --reedit-message" the same template.
No change in behavior.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
sequencer.c | 70 +++++++++++++++++++++++++++++------------------------
sequencer.h | 30 +++++++++++++++++++++++
2 files changed, 69 insertions(+), 31 deletions(-)
diff --git a/sequencer.c b/sequencer.c
index 1355a99a09..3c704fd5ab 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -1880,18 +1880,38 @@ static int is_pick_or_similar(enum todo_command command)
}
}
-enum todo_item_flags {
- TODO_EDIT_MERGE_MSG = (1 << 0),
- TODO_REPLACE_FIXUP_MSG = (1 << 1),
- TODO_EDIT_FIXUP_MSG = (1 << 2),
-};
-
static const char first_commit_msg_str[] = N_("This is the 1st commit message:");
static const char nth_commit_msg_fmt[] = N_("This is the commit message #%d:");
static const char skip_first_commit_msg_str[] = N_("The 1st commit message will be skipped:");
static const char skip_nth_commit_msg_fmt[] = N_("The commit message #%d will be skipped:");
static const char combined_commit_msg_fmt[] = N_("This is a combination of %d commits.");
+void add_squash_combination_header(struct strbuf *buf, int n)
+{
+ strbuf_addf(buf, "%s ", comment_line_str);
+ strbuf_addf(buf, _(combined_commit_msg_fmt), n);
+}
+
+void add_squash_message_header(struct strbuf *buf, int n, int skip)
+{
+ strbuf_addf(buf, "%s ", comment_line_str);
+ if (n == 1)
+ strbuf_addstr(buf, skip ? _(skip_first_commit_msg_str) :
+ _(first_commit_msg_str));
+ else
+ strbuf_addf(buf, skip ? _(skip_nth_commit_msg_fmt) :
+ _(nth_commit_msg_fmt), n);
+}
+
+size_t squash_subject_comment_len(const char *body, int squashing)
+{
+ if (starts_with(body, "amend!") ||
+ (squashing && (starts_with(body, "squash!") ||
+ starts_with(body, "fixup!"))))
+ return commit_subject_length(body);
+ return 0;
+}
+
static int is_fixup_flag(enum todo_command command, unsigned flag)
{
return command == TODO_FIXUP && ((flag & TODO_REPLACE_FIXUP_MSG) ||
@@ -2005,20 +2025,13 @@ static int append_squash_message(struct strbuf *buf, const char *body,
{
struct replay_ctx *ctx = opts->ctx;
const char *fixup_msg;
- size_t commented_len = 0, fixup_off;
- /*
- * amend is non-interactive and not normally used with fixup!
- * or squash! commits, so only comment out those subjects when
- * squashing commit messages.
- */
- if (starts_with(body, "amend!") ||
- ((command == TODO_SQUASH || seen_squash(ctx)) &&
- (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
- commented_len = commit_subject_length(body);
+ size_t commented_len, fixup_off;
+
+ commented_len = squash_subject_comment_len(body,
+ command == TODO_SQUASH || seen_squash(ctx));
- strbuf_addf(buf, "\n%s ", comment_line_str);
- strbuf_addf(buf, _(nth_commit_msg_fmt),
- ++ctx->current_fixup_count + 1);
+ strbuf_addch(buf, '\n');
+ add_squash_message_header(buf, ++ctx->current_fixup_count + 1, 0);
strbuf_addstr(buf, "\n\n");
strbuf_add_commented_lines(buf, body, commented_len, comment_line_str);
/* buf->buf may be reallocated so store an offset into the buffer */
@@ -2083,9 +2096,8 @@ static int update_squash_messages(struct repository *r,
eol = !starts_with(buf.buf, comment_line_str) ?
buf.buf : strchrnul(buf.buf, '\n');
- strbuf_addf(&header, "%s ", comment_line_str);
- strbuf_addf(&header, _(combined_commit_msg_fmt),
- ctx->current_fixup_count + 2);
+ add_squash_combination_header(&header,
+ ctx->current_fixup_count + 2);
strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
strbuf_release(&header);
if (is_fixup_flag(command, flag) && !seen_squash(ctx))
@@ -2109,12 +2121,9 @@ static int update_squash_messages(struct repository *r,
repo_unuse_commit_buffer(r, head_commit, head_message);
return error(_("cannot write '%s'"), rebase_path_fixup_msg());
}
- strbuf_addf(&buf, "%s ", comment_line_str);
- strbuf_addf(&buf, _(combined_commit_msg_fmt), 2);
- strbuf_addf(&buf, "\n%s ", comment_line_str);
- strbuf_addstr(&buf, is_fixup_flag(command, flag) ?
- _(skip_first_commit_msg_str) :
- _(first_commit_msg_str));
+ add_squash_combination_header(&buf, 2);
+ strbuf_addch(&buf, '\n');
+ add_squash_message_header(&buf, 1, is_fixup_flag(command, flag));
strbuf_addstr(&buf, "\n\n");
if (is_fixup_flag(command, flag))
strbuf_add_commented_lines(&buf, body, strlen(body),
@@ -2133,9 +2142,8 @@ static int update_squash_messages(struct repository *r,
if (command == TODO_SQUASH || is_fixup_flag(command, flag)) {
res = append_squash_message(&buf, body, command, opts, flag);
} else if (command == TODO_FIXUP) {
- strbuf_addf(&buf, "\n%s ", comment_line_str);
- strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
- ++ctx->current_fixup_count + 1);
+ strbuf_addch(&buf, '\n');
+ add_squash_message_header(&buf, ++ctx->current_fixup_count + 1, 1);
strbuf_addstr(&buf, "\n\n");
strbuf_add_commented_lines(&buf, body, strlen(body),
comment_line_str);
diff --git a/sequencer.h b/sequencer.h
index 64a9c7fb1b..b01f897020 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -119,6 +119,13 @@ enum todo_command {
TODO_COMMENT
};
+/* Bits for the "flags" member of struct todo_item */
+enum todo_item_flags {
+ TODO_EDIT_MERGE_MSG = (1 << 0),
+ TODO_REPLACE_FIXUP_MSG = (1 << 1),
+ TODO_EDIT_FIXUP_MSG = (1 << 2),
+};
+
struct todo_item {
enum todo_command command;
struct commit *commit;
@@ -208,6 +215,29 @@ int todo_list_rearrange_squash(struct todo_list *todo_list);
*/
void append_signoff(struct strbuf *msgbuf, size_t ignore_footer, unsigned flag);
+/*
+ * Append the "This is a combination of N commits." banner that "git rebase
+ * -i" writes at the top of a squashed commit's message, commented out with
+ * the comment character.
+ */
+void add_squash_combination_header(struct strbuf *buf, int n);
+
+/*
+ * Append the header (1-based N) that "git rebase -i" writes above each message
+ * when squashing, commented out with the comment character. With SKIP it reads
+ * "The ... commit message will be skipped" for a message that is dropped (a
+ * fixup), otherwise "This is the ... commit message".
+ */
+void add_squash_message_header(struct strbuf *buf, int n, int skip);
+
+/*
+ * Return the length of the leading subject of BODY when it should be commented
+ * out in a squash message, or 0 otherwise. An "amend!" subject always
+ * qualifies; "squash!" and "fixup!" subjects only when SQUASHING, since a
+ * plain fixup chain keeps them.
+ */
+size_t squash_subject_comment_len(const char *body, int squashing);
+
void append_conflicts_hint(struct index_state *istate,
struct strbuf *msgbuf, enum commit_msg_cleanup_mode cleanup_mode);
enum commit_msg_cleanup_mode get_cleanup_mode(const char *cleanup_arg,
--
gitgitgadget
^ permalink raw reply related
* [PATCH v10 5/5] history: re-edit a squash with every message
From: Harald Nordgren via GitGitGadget @ 2026-07-20 8:27 UTC (permalink / raw)
To: git
Cc: Phillip Wood, D. Ben Knoble, Patrick Steinhardt, Matt Hunter,
Harald Nordgren, Harald Nordgren
In-Reply-To: <pull.2337.v10.git.git.1784536024.gitgitgadget@gmail.com>
From: Harald Nordgren <haraldnordgren@gmail.com>
By default "git history squash" reuses the oldest commit's message, or
the replacement body from an amend! commit targeting it. When
--reedit-message is given it only reopened that selected message, so the
messages of the other commits in the range were lost.
Gather the message of every commit in the range and build the same editor
template that "git rebase -i --autosquash" shows for a squash, reusing
add_squash_combination_header(), add_squash_message_header() and
squash_subject_comment_len(). Feed the range through
todo_list_rearrange_squash() so that each fixup!, squash! or amend! is
grouped under the commit it targets rather than shown in commit order,
exactly as autosquash would arrange them.
Only the message text differs, the changes are always folded in. A fixup!
message is commented out in full under a "will be skipped" header, a
squash! keeps its body with only the marker subject commented, and an
amend! replaces its target's message unless a squash! already folded into
that target, in which case it behaves like a squash!.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
Documentation/git-history.adoc | 22 +++-
builtin/history.c | 104 +++++++++++++++++
t/t3455-history-squash.sh | 201 +++++++++++++++++++++++++++++++++
3 files changed, 321 insertions(+), 6 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index e1e930f355..6f3b031d2a 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -134,11 +134,12 @@ already on `topic`. Rev-list options may also be given, but any that would
change how the range is walked are overridden with a warning.
+
The oldest commit's message is preserved by default, except that an `amend!`
-commit targeting it replaces its message. Specify `--reedit-message` to edit
-the resulting message. A merge commit inside the range is folded like any
-other, but the range must have a single base, so a range that reaches more
-than one entry point (for example a side branch that forked before the range
-and was later merged into it) is rejected.
+commit targeting it replaces its message. With `--reedit-message`, an editor
+opens pre-filled with the messages of all the folded commits so you can
+combine them. A merge commit inside the range is folded like any other, but
+the range must have a single base, so a range that reaches more than one entry
+point (for example a side branch that forked before the range and was later
+merged into it) is rejected.
+
A `fixup!`, `squash!`, or `amend!` commit is refused unless the commit it
targets is also in the range, so the fold does not silently absorb a
@@ -146,6 +147,14 @@ marker meant for a commit outside it. As an exception, a range made up entirely
of markers for one target is combined into a single commit, keeping the last
`amend!` message if there is one.
+
+With `--reedit-message` the template mirrors `git rebase -i --autosquash`:
+each `fixup!`, `squash!`, or `amend!` is grouped under the commit it
+targets rather than shown in commit order. A `fixup!` message is dropped
+(commented out in full), a `squash!` keeps its body with only the marker
+subject commented, and an `amend!` replaces its target's message, unless
+a `squash!` folded into that target first, in which case it keeps its
+body like a `squash!`.
++
A branch or tag that points at a commit inside the range would be left
dangling once those commits are folded away, so with the default
`--update-refs=branches` the command refuses. Rerun with
@@ -162,7 +171,8 @@ OPTIONS
ref updates is generally safe.
`--reedit-message`::
- Open an editor to modify the rewritten commit's message.
+ Open an editor to modify the rewritten commit's message. For `squash`
+ the editor is pre-filled with the messages of all the folded commits.
`--empty=(drop|keep|abort)`::
Control what happens when a commit becomes empty as a result of the
diff --git a/builtin/history.c b/builtin/history.c
index 423c8beaaf..2542ea33a0 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1255,6 +1255,102 @@ static int find_interior_ref(const struct reference *ref, void *cb_data)
return 0;
}
+static bool amend_replaces_target(struct todo_list *todo, int target)
+{
+ int i;
+
+ for (i = target + 1; i < todo->nr &&
+ todo->items[i].command != TODO_PICK; i++) {
+ if (todo->items[i].command == TODO_SQUASH)
+ return false;
+ if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
+ return true;
+ }
+ return false;
+}
+
+static int build_squash_message(struct repository *repo,
+ struct commit *base,
+ struct commit *tip,
+ struct strbuf *out)
+{
+ struct rev_info revs;
+ struct commit *commit;
+ struct strvec args = STRVEC_INIT;
+ struct todo_list todo = TODO_LIST_INIT;
+ struct replay_opts opts = REPLAY_OPTS_INIT;
+ int i, nr_commits, ret;
+
+ repo_init_revisions(repo, &revs, NULL);
+ strvec_push(&args, "ignored");
+ strvec_push(&args, "--reverse");
+ strvec_push(&args, "--topo-order");
+ strvec_pushf(&args, "%s..%s", oid_to_hex(&base->object.oid),
+ oid_to_hex(&tip->object.oid));
+ setup_revisions_from_strvec(&args, &revs, NULL);
+
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto out;
+ }
+
+ while ((commit = get_revision(&revs)))
+ strbuf_addf(&todo.buf, "pick %s\n",
+ oid_to_hex(&commit->object.oid));
+
+ if (todo_list_parse_insn_buffer(repo, &opts, todo.buf.buf, &todo) < 0 ||
+ todo_list_rearrange_squash(&todo) < 0) {
+ ret = error(_("could not prepare the squash message"));
+ goto out;
+ }
+
+ nr_commits = todo.nr;
+ for (i = 0; i < nr_commits; i++) {
+ struct todo_item *item = &todo.items[i];
+ const char *message, *body;
+ size_t commented_len;
+ bool skip, squashing;
+
+ squashing = item->command == TODO_SQUASH ||
+ (item->flags & TODO_REPLACE_FIXUP_MSG);
+ if (item->command == TODO_PICK)
+ skip = amend_replaces_target(&todo, i);
+ else
+ skip = !squashing;
+
+ message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
+ find_commit_subject(message, &body);
+
+ if (skip)
+ commented_len = strlen(body);
+ else if (squashing)
+ commented_len = squash_subject_comment_len(body, 1);
+ else
+ commented_len = 0;
+
+ if (!i)
+ add_squash_combination_header(out, nr_commits);
+ strbuf_addch(out, '\n');
+ add_squash_message_header(out, i + 1, skip);
+ strbuf_addstr(out, "\n\n");
+ strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
+ strbuf_addstr(out, body + commented_len);
+ strbuf_complete_line(out);
+
+ repo_unuse_commit_buffer(repo, item->commit, message);
+ }
+
+ ret = 0;
+
+out:
+ todo_list_release(&todo);
+ replay_opts_release(&opts);
+ reset_revision_walk();
+ release_revisions(&revs);
+ strvec_clear(&args);
+ return ret;
+}
+
static int cmd_history_squash(int argc,
const char **argv,
const char *prefix,
@@ -1338,6 +1434,14 @@ static int cmd_history_squash(int argc,
}
}
+ if (flags & COMMIT_TREE_EDIT_MESSAGE) {
+ strbuf_reset(&message);
+ ret = build_squash_message(repo, base, tip, &message);
+ if (ret < 0)
+ goto out;
+ message_template = message.buf;
+ }
+
ret = setup_revwalk(repo, action, tip, &revs);
if (ret < 0)
goto out;
diff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh
index 9c362f3094..6fce924327 100755
--- a/t/t3455-history-squash.sh
+++ b/t/t3455-history-squash.sh
@@ -271,6 +271,207 @@ test_expect_success 'preserves authorship of the oldest commit' '
test_cmp expect actual
'
+test_expect_success '--reedit-message offers every folded-in message' '
+ git reset --hard start &&
+ stage_file b &&
+ git commit -m "re-one subject" -m "re-one body line" &&
+ test_commit --no-tag re-two file c &&
+ test_commit re-three file d &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited &&
+ echo combined >"$1"
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 3 commits.
+ # This is the 1st commit message:
+
+ re-one subject
+
+ re-one body line
+
+ # This is the commit message #2:
+
+ re-two
+
+ # This is the commit message #3:
+
+ re-three
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_subjects -1 <<-\EOF
+ combined
+ EOF
+'
+
+test_expect_success '--reedit-message handles fixup!, squash! and amend! like rebase' '
+ git reset --hard start &&
+ test_commit --no-tag mark-base file b &&
+ stage_file c &&
+ commit_with_message "fixup! mark-base\n\nfixup body\n" &&
+ stage_file d &&
+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
+ stage_file e &&
+ commit_with_message "amend! mark-base\n\namended message\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 4 commits.
+ # This is the 1st commit message:
+
+ mark-base
+
+ # The commit message #2 will be skipped:
+
+ # fixup! mark-base
+ #
+ # fixup body
+
+ # This is the commit message #3:
+
+ # squash! mark-base
+
+ squash remark
+
+ # This is the commit message #4:
+
+ # amend! mark-base
+
+ amended message
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_messages -1 <<-\EOF
+ mark-base
+
+ squash remark
+
+ amended message
+
+ EOF
+'
+
+test_expect_success '--reedit-message groups fixups under their targets' '
+ git reset --hard start &&
+ test_commit --no-tag alpha file a1 &&
+ test_commit --no-tag beta file b1 &&
+ stage_file a2 &&
+ commit_with_message "fixup! alpha\n" &&
+ stage_file b2 &&
+ commit_with_message "fixup! beta\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 4 commits.
+ # This is the 1st commit message:
+
+ alpha
+
+ # The commit message #2 will be skipped:
+
+ # fixup! alpha
+
+ # This is the commit message #3:
+
+ beta
+
+ # The commit message #4 will be skipped:
+
+ # fixup! beta
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited
+'
+
+test_expect_success '--reedit-message lets amend! replace its target message' '
+ git reset --hard start &&
+ test_commit --no-tag mark-base file b &&
+ stage_file c &&
+ commit_with_message "amend! mark-base\n\namended message\n" &&
+ stage_file d &&
+ commit_with_message "squash! mark-base\n\nsquash remark\n" &&
+
+ write_script editor <<-\EOF &&
+ cat "$1" >edited
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ git history squash --reedit-message start.. &&
+
+ cat >expect <<-EOF &&
+ # This is a combination of 3 commits.
+ # The 1st commit message will be skipped:
+
+ # mark-base
+
+ # This is the commit message #2:
+
+ # amend! mark-base
+
+ amended message
+
+ # This is the commit message #3:
+
+ # squash! mark-base
+
+ squash remark
+
+ # Please enter the commit message for the squash changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # modified: file
+ #
+ EOF
+ test_cmp expect edited &&
+ check_log_messages -1 <<-\EOF
+ amended message
+
+ squash remark
+
+ EOF
+'
+
+test_expect_success '--reedit-message aborts on an empty message' '
+ git reset --hard three &&
+ head_before=$(git rev-parse HEAD) &&
+
+ write_script editor <<-\EOF &&
+ >"$1"
+ EOF
+ test_set_editor "$(pwd)/editor" &&
+ test_must_fail git history squash --reedit-message start.. &&
+
+ test_cmp_rev "$head_before" HEAD
+'
+
test_expect_success '--update-refs=head only moves HEAD' '
git reset --hard three &&
git branch -f other HEAD &&
--
gitgitgadget
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox