Home / Alt manpages / git-send-pack(1)

  • git-send-pack(1)
  • User command
  • linux

Push a Specific Ref Safely with git-send-pack

You will push one local branch to a bare Git repository, preview the change first, and verify the remote ref afterwards. This is a low-level operation: Git's own manual says that git push is normally the higher-level command to use. Reach for git send-pack when you need its direct ref selection, receive-pack control, or protocol-level behaviour.

Allow about 10 minutes for the first run. You need Git 2.43.0 or newer in the same command environment, a source repository with at least one commit, and write access to the destination repository. The examples use a local bare repository, so they do not need elevated privileges. Do not put sudo in front of these commands: changing ownership of a repository can create a separate problem.

1. Check the installed command and repository state

Run these ordinary, read-only checks from the source repository. The installed manpage identifies this machine's command as Git 2.43.0. Replace the example branch name only after checking which ref you actually intend to publish.

$ git --version
git version 2.43.0
$ git status --short
$ git show-ref --verify refs/heads/main

An empty git status --short means there are no uncommitted changes to distract you. If your branch is not called main, list local heads with git for-each-ref --format='%(refname)' refs/heads/ and use the full name that exists. Stop here if the source ref is wrong; a refspec cannot push an object that is not present locally.

2. Prepare a harmless local destination

For a reproducible test, create a bare repository outside the working tree. A bare repository has no checked-out files; its refs are the state that a server would receive. This step changes only the new directory under /tmp.

$ DEST=$(mktemp -d /tmp/git-send-pack-demo.XXXXXX)
$ git init --bare "$DEST/remote.git"
Initialized empty Git repository in /tmp/git-send-pack-demo.XXXXXX/remote.git/
$ git -C "$DEST/remote.git" show-ref

The final command prints nothing because the destination has no refs yet. In a real deployment, the destination is commonly written as [email protected]:/srv/git/project.git; the host prefix makes Git invoke git-receive-pack through SSH. Confirm the path and account before doing a real push.

3. Preview one explicit ref update

Use a source-to-destination pair to make the operation unambiguous. The destination below is a local path, so no SSH service is involved.

$ git send-pack --dry-run --verbose \
    "$DEST/remote.git" \
    refs/heads/main:refs/heads/main

The dry run performs the negotiation without actually updating the destination. On an empty destination, Git reports a new branch, although the exact progress wording can vary with terminal and Git settings. Verify that the destination is still empty:

$ git -C "$DEST/remote.git" show-ref --verify refs/heads/main
fatal: 'refs/heads/main' - not a valid ref

A single name such as main is shorthand for main:main. Full ref names are easier to audit in scripts. The source side must resolve to exactly one local ref. A new destination ref must start with refs/, which is why the example spells it out.

Checkpoint: the preview is safe

At this point, confirm three things before removing --dry-run: the source commit is the one you expect, the destination path is correct, and the destination ref is the name you intend to update. If any answer is uncertain, stop and inspect with git log --oneline --decorate -5 refs/heads/main.

4. Send the ref and verify the result

Remove only the preview flag. This is the first command in the guide that changes remote state.

$ git send-pack --verbose \
    "$DEST/remote.git" \
    refs/heads/main:refs/heads/main
$ git -C "$DEST/remote.git" show-ref --verify refs/heads/main
<commit-id> refs/heads/main

Use this verification command with the actual object ID if you need a strict check:

$ test "$(git -C "$DEST/remote.git" rev-parse refs/heads/main)" = \
    "$(git rev-parse refs/heads/main)" && echo "remote matches local main"
remote matches local main

5. Map other refs without guessing

The part before the colon is the local source; the part after it is the remote destination. For example, this publishes a local topic under a different remote name:

$ git send-pack --dry-run \
    [email protected]:/srv/git/project.git \
    refs/heads/topic:refs/heads/review/topic

Without --all and without explicit refs, the command updates heads that exist on both sides. That default is easy to overlook, so automation should normally name every ref it means to change. With --all, all locally existing heads are transferred and explicit refs cannot be supplied at the same time. Do not confuse --all with --mirror: mirroring is a broader operation and deserves a separate review of every ref it may affect.

6. Handle rejected updates safely

By default, the destination must be empty or an ancestor of the source. This fast-forward check protects commits that exist only on the remote. If another contributor has advanced the branch, the send fails rather than overwriting it. Fetch or inspect the remote, reconcile the histories in your normal workflow, and retry the explicit refspec.

Warning

--force disables that check for every ref in this invocation. A leading plus disables it for only one ref, for example +refs/heads/main:refs/heads/main. Both forms can make commits disappear from the branch name. Use them only with an agreed recovery plan, and record the old object ID first:

$ git -C "$DEST/remote.git" rev-parse refs/heads/main
<old-commit-id>
$ git send-pack --dry-run "$DEST/remote.git" \
    +refs/heads/main:refs/heads/main

If a force update has already happened, the old commit may still be recoverable from another clone or reflog, but that is not guaranteed. Prefer the dry run and the narrow plus-prefixed refspec. For several refs, --atomic asks the receiver to update them as one transaction: if one update fails, none should change. The remote must support atomic transactions.

7. Finish and clean up the test

Once the verification passes, remove the temporary demonstration directory. This is destructive, but it contains only the bare repository created in step 2:

$ rm -rf -- "$DEST"
$ test ! -e "$DEST" && echo "temporary repository removed"
temporary repository removed

For a real remote, there is no local undo command for a successful push. The recovery action is another carefully reviewed ref update, usually after preserving the current remote object ID and agreeing whether the update should be a normal fast-forward or a force update.

Done means

  • The source ref was checked and resolved to the intended commit.
  • --dry-run showed the expected destination before any update.
  • The explicit source and destination refs were sent successfully.
  • The destination ref resolves to the expected object ID.
  • No force update was used without preserving the old object ID and planning recovery.