Subject: [PATCH 4/4] doc: git-push: rewrite refspec specification

From: "Julia Evans via GitGitGadget" <[email-masked]>
Date: Tue, 26 Aug 2025 20:40:22 +0000

From: Julia Evans <[email-masked]>

- Originally it said that a refspec was `+<src>:<dst>`, but then later
  contradicted itself by saying that the `:<dst>` is optional.
  Mention that `:<dst>` is optional much earlier.
- Put the complex sets of rules about different refspec forms
  in lists instead of in long paragraphs of prose
- Add examples for the various types of refspecs
  (negative, deletion, pattern, etc)
- Previously `*` and `^` were not mentioned, mention them
- Explain what `+` does earlier
- Remove "might be added in the future" (it's a given that software
  might change in the future)

Signed-off-by: Julia Evans <[email-masked]>
---
 Documentation/git-push.adoc | 164 ++++++++++++++++++------------------
 1 file changed, 82 insertions(+), 82 deletions(-)

diff --git a/Documentation/git-push.adoc b/Documentation/git-push.adoc
index 0232195515c9..78d433c60c51 100644
--- a/Documentation/git-push.adoc
+++ b/Documentation/git-push.adoc
@@ -57,77 +57,74 @@ OPTIONS[[OPTIONS]]
 
 <refspec>...::
 	Specify what destination ref to update with what source object.
-	The format of a <refspec> parameter is an optional plus
-	`+`, followed by the source object <src>, followed
-	by a colon `:`, followed by the destination ref <dst>.
-+
-The <src> is often the name of the branch you would want to push, but
-it can be any arbitrary "SHA-1 expression", such as `master~4` or
-`HEAD` (see linkgit:gitrevisions[7]).
-+
-The <dst> tells which ref on the remote side is updated with this
-push. Arbitrary expressions cannot be used here, an actual ref must
-be named.
-If `git push [<repository>]` without any `<refspec>` argument is set to
-update some ref at the destination with `<src>` with
-`remote.<repository>.push` configuration variable, `:<dst>` part can
-be omitted--such a push will update a ref that `<src>` normally updates
-without any `<refspec>` on the command line.  Otherwise, missing
-`:<dst>` means to update the same ref as the `<src>`.
-+
-If <dst> doesn't start with `refs/` (e.g. `refs/heads/master`) we will
-try to infer where in `refs/*` on the destination <repository> it
-belongs based on the type of <src> being pushed and whether <dst>
-is ambiguous.
 +
---
-* If <dst> unambiguously refers to a ref on the <repository> remote,
-  then push to that ref.
-
-* If <src> resolves to a ref starting with refs/heads/ or refs/tags/,
-  then prepend that to <dst>.
-
-* Other ambiguity resolutions might be added in the future, but for
-  now any other cases will error out with an error indicating what we
-  tried, and depending on the `advice.pushUnqualifiedRefname`
-  configuration (see linkgit:git-config[1]) suggest what refs/
-  namespace you may have wanted to push to.
-
---
-+
-The object referenced by <src> is used to update the <dst> reference
-on the remote side. Whether this is allowed depends on where in
-`refs/*` the <dst> reference lives as described in detail below, in
-those sections "update" means any modifications except deletes, which
-as noted after the next few sections are treated differently.
-+
-The `refs/heads/*` namespace will only accept commit objects, and
-updates only if they can be fast-forwarded.
-+
-The `refs/tags/*` namespace will accept any kind of object (as
-commits, trees and blobs can be tagged), and any updates to them will
-be rejected.
-+
-It's possible to push any type of object to any namespace outside of
-`refs/{tags,heads}/*`. In the case of tags and commits, these will be
-treated as if they were the commits inside `refs/heads/*` for the
-purposes of whether the update is allowed.
-+
-I.e. a fast-forward of commits and tags outside `refs/{tags,heads}/*`
-is allowed, even in cases where what's being fast-forwarded is not a
-commit, but a tag object which happens to point to a new commit which
-is a fast-forward of the commit the last tag (or commit) it's
-replacing. Replacing a tag with an entirely different tag is also
-allowed, if it points to the same commit, as well as pushing a peeled
-tag, i.e. pushing the commit that existing tag object points to, or a
-new tag object which an existing commit points to.
-+
-Tree and blob objects outside of `refs/{tags,heads}/*` will be treated
-the same way as if they were inside `refs/tags/*`, any update of them
-will be rejected.
-+
-All of the rules described above about what's not allowed as an update
-can be overridden by adding an the optional leading `+` to a refspec
+The format for a refspec is [+]<src>[:<dst>], for example `main`,
+`main:other`, or `HEAD^:refs/heads/main`.
++
+The `<src>` is often the name of the local branch to push, but it can be
+any arbitrary "SHA-1 expression" (see linkgit:gitrevisions[7]).
++
+The `<dst>` determines what to update on the remote side. It must be the
+name of a branch, tag, or other ref, not an arbitrary expression.
+`:<dst>` is optional.
++
+`+` is optional and does the same thing as `--force`.
++
+You can write a refspec using the fully expanded form (for
+example `main:refs/heads/main`) which specifies the exact source
+and destination, or with a shorter form (for example `main` or
+`main:other`). Here are the rules for how refspecs are expanded,
+as well as various other special refspec forms:
++
+ 1. `<src>` without a `:<dst>` means to update the same ref as the
+	`<src>`, unless the `remote.<repository>.push` configuration specifies a
+	different <dst>. For example, if `main` is a branch, then the refspec
+    `main` expands to `main:refs/heads/main`.
+ 2. If <dst> unambiguously refers to a ref on the <repository> remote,
+    then expand it to that ref. For example, if `v1.0` is a tag on the
+    remote, then `HEAD:v1.0` expands to `HEAD:refs/tags/v1.0`.
+ 3. If <src> resolves to a ref starting with refs/heads/ or refs/tags/,
+    then prepend that to <dst>. For example, if `main` is a branch, then
+    `main:other` expands to `main:refs/heads/other`
+ 4. The special refspec `:` (or `+:` to allow non-fast-forward updates)
+    directs Git to push "matching" branches: for every branch that exists on
+    the local side, the remote side is updated if a branch of the same name
+    already exists on the remote side.
+ 5. `tag <tag>` expands to `refs/tags/<tag>:refs/tags/<tag>`.
+ 6. <src> may contain a * to indicate a simple pattern match.
+    This works like a glob that matches any ref matching the pattern.
+    There must be only one * in both the <src> and <dst>.
+    It will map refs to the destination by replacing the * with the
+    contents matched from the source. For example, `refs/heads/*:refs/heads/*`
+    will push all branches.
+ 7. A refspec starting with ^ is a negative refspec.
+    This specifies refs to exclude. A ref will be considered to
+    match if it matches at least one positive refspec, and does not
+    match any negative refspec. Negative refspecs can be pattern refspecs.
+    They must only contain a <src>.
+    Fully spelled out hex object names are also not supported.
+    For example, `git push origin 'refs/heads/*' '^refs/heads/dev-*'`
+    will push all branches except for those starting with `dev-`
+ 8. If `<src>` is empty, it deletes the <dst> ref from the remote
+    repository. For example, `git push origin :dev` will
+    delete the `dev` branch.
+    Deletions are always accepted without a leading `+` in the
+    refspec (or `--force`), except when forbidden by configuration or hooks.
+    See `receive.denyDeletes` in linkgit:git-config[1] and `pre-receive` and
+    `update` in linkgit:githooks[5].
+ 9. If the refspec can't be expanded unambiguously, error
+    out with an error indicating what was
+    tried, and depending on the `advice.pushUnqualifiedRefname`
+    configuration (see linkgit:git-config[1]) suggest what refs/
+    namespace you may have wanted to push to.
+
++
+Not all updates are allowed: it depends on what kind of destination
+you're pushing to. In the following rules "update" means any
+modifications except deletes, which as noted above are treated differently.
++
+All of these rules
+can be overridden by adding the optional leading `+` to a refspec
 (or using `--force` command line option). The only exception to this
 is that no amount of forcing will make the `refs/heads/*` namespace
 accept a non-commit object. Hooks and configuration can also override
@@ -135,18 +132,21 @@ or amend these rules, see e.g. `receive.denyNonFastForwards` in
 linkgit:git-config[1] and `pre-receive` and `update` in
 linkgit:githooks[5].
 +
-Pushing an empty <src> allows you to delete the <dst> ref from the
-remote repository. Deletions are always accepted without a leading `+`
-in the refspec (or `--force`), except when forbidden by configuration
-or hooks. See `receive.denyDeletes` in linkgit:git-config[1] and
-`pre-receive` and `update` in linkgit:githooks[5].
-+
-The special refspec `:` (or `+:` to allow non-fast-forward updates)
-directs Git to push "matching" branches: for every branch that exists on
-the local side, the remote side is updated if a branch of the same name
-already exists on the remote side.
-+
-`tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`.
+1. If the destination is a **branch** (`refs/heads/*`): the source must
+   be a commit object, and only fast-forward updates are allowed.
+2. If the destination is a **tag** (`refs/tags/*`):  the source can
+   be any object (as commits, trees and blobs can be tagged), and any
+   updates to them will be rejected.
+3. For destinations outside of `refs/{tags,heads}/*`:
+   * If the source is a tree or blob object, any updates will be rejected
+   * If the source is a tag or commit object, any fast-forward update
+     is allowed, even in cases where what's being fast-forwarded is not a
+     commit, but a tag object which happens to point to a new commit which
+     is a fast-forward of the commit the last tag (or commit) it's
+     replacing. Replacing a tag with an entirely different tag is also
+     allowed, if it points to the same commit, as well as pushing a peeled
+     tag, i.e. pushing the commit that existing tag object points to, or a
+     new tag object which an existing commit points to.
 
 --all::
 --branches::
-- 
gitgitgadget