Home / Alt manpages / git-commit-tree(1)

  • git-commit-tree(1)
  • User command
  • linux

Build a Git Commit Object Safely with git-commit-tree

You will finish with a small, inspectable Git repository containing a root commit and a child commit created from tree object IDs. You will also see how to attach a branch name to the result, which is the part that makes a commit reachable from normal Git commands.

Allow about fifteen minutes. You need Git and a shell. The examples use Git 2.43.0 from the installed git-man package, and run in a temporary repository. No command needs elevated privileges. This is plumbing: for everyday commits, use git commit.

1. Keep the object and the reference separate

git-commit-tree creates a commit object from an existing tree object and prints the new object ID. It does not, by itself, move HEAD, update a branch, stage files or change the index. A tree records a directory snapshot; a commit adds parents, author and committer details, dates and a log message.

That distinction is the main trap. A successful command can leave you with a perfectly valid commit that git log cannot find from your current branch. Treat the printed ID as valuable output until you have deliberately stored it in a reference.

Checkpoint: confirm the installed version before experimenting:

$ git --version
git version 2.43.0
$ git help commit-tree

The second command opens the local manual. If it waits for a pager, press q to return to the shell.

2. Create a tree from the index

Start in a disposable directory. The directory is only a test workspace, so this does not touch an existing project:

$ demo=$(mktemp -d /tmp/commit-tree-demo.XXXXXX)
$ cd "$demo"
$ git init
Initialized empty Git repository in /tmp/commit-tree-demo.../.git/
$ git config user.name "Example Operator"
$ git config user.email "[email protected]"
$ printf '%s\n' 'first snapshot' > README.txt
$ git add README.txt
$ tree=$(git write-tree)
$ printf 'tree: %s\n' "$tree"
tree: 3ab2e03dea1bc23d0ef6715fd6395ce1629f1889

Your tree ID will differ. git write-tree serialises the current index into a tree object. The file is not committed yet, and changing the working copy after this point does not change the tree you have already written.

Do not skip git add when testing this workflow. The tree comes from the index, not directly from every file in the working directory. An empty or stale index produces a snapshot different from the one you had in mind.

3. Create a root commit with a message

A root commit has no parent. Pass the tree ID and a message with -m:

$ root=$(git commit-tree "$tree" -m "Record the first snapshot")
$ printf 'root commit: %s\n' "$root"
root commit: 92b88e6de9ff0828250526f0ad39ed69a04bc271
$ git cat-file -p "$root"
tree 3ab2e03dea1bc23d0ef6715fd6395ce1629f1889
author Example Operator <[email protected]> 1790210810 +0100
committer Example Operator <[email protected]> 1790210810 +0100

Record the first snapshot

The IDs and timestamps will differ. The object has a tree, author and committer headers, then the message. Because no -p option was supplied, it has no parent and is a root commit.

Without -m or -F, git-commit-tree reads the message from standard input and waits for end-of-file. That is easy to mistake for a hung command. Use -m for a known paragraph, or -F message.txt when the message is maintained in a file. Multiple -m and -F options become separate paragraphs in the order supplied.

4. Create a child commit with a parent

Change the snapshot, update the index, write another tree, then pass the earlier commit with -p:

$ printf '%s\n' 'second snapshot' >> README.txt
$ git add README.txt
$ tree2=$(git write-tree)
$ child=$(git commit-tree "$tree2" -p "$root" -m "Add the second snapshot")
$ git cat-file -p "$child"
tree a9792f6f33554fc2c75e18a01ea16956a1e6daac
parent 92b88e6de9ff0828250526f0ad39ed69a04bc271
author Example Operator <[email protected]> 1790210810 +0100
committer Example Operator <[email protected]> 1790210810 +0100

Add the second snapshot

The parent line is the history link. One parent makes this an ordinary commit. Supplying several -p options creates a merge commit with several parents. Supplying none creates another root commit.

Checkpoint: ask Git to verify the object type and relationship:

$ git cat-file -t "$child"
commit
$ git diff-tree --no-commit-id --name-status -r "$root" "$child"
M       README.txt

5. Make the commit reachable from a branch

Only now attach a reference. This changes repository metadata, so check the target name first. The command below creates or moves a deliberately named local branch:

$ git show-ref --verify --quiet refs/heads/manual-commit && echo 'branch already exists' || echo 'branch is unused'
branch is unused
$ git update-ref refs/heads/manual-commit "$child"
$ git log --oneline --decorate --graph manual-commit
* 2c68feeb (manual-commit) Add the second snapshot
* 92b88e6 Record the first snapshot

Do not point an existing branch at a new object without checking its current value. A safer update includes the old ID as the final argument, so Git refuses if another process moved the branch in the meantime:

$ old=$(git rev-parse refs/heads/manual-commit)
$ git update-ref refs/heads/manual-commit "$child" "$old"

This example has no service impact and does not require root. It does change the branch reference. To undo that specific test change while the branch still points at the child, delete the reference:

$ git update-ref -d refs/heads/manual-commit "$child"
$ git show-ref --verify refs/heads/manual-commit
fatal: 'refs/heads/manual-commit' - not a valid ref

The commit objects may remain temporarily as unreachable objects after the reference is removed. Do not run aggressive garbage collection as part of an experiment unless you have confirmed that no other reference needs those objects.

6. Set identity and dates deliberately

Commit identity comes from Git configuration and can be overridden by the usual author and committer environment variables. In automation, verify the values before creating an object:

$ git var GIT_AUTHOR_IDENT
Example Operator <[email protected]> 1790210810 +0100
$ git var GIT_COMMITTER_IDENT
Example Operator <[email protected]> 1790210810 +0100

GIT_AUTHOR_DATE and GIT_COMMITTER_DATE accept Git's internal timestamp form, RFC 2822 and ISO 8601 forms. A forced date becomes part of the commit identity, so use it only when a reproducible or imported history requires it. Otherwise let Git record the current time.

Signing is also explicit. -S or --gpg-sign asks Git to sign the commit, while --no-gpg-sign cancels an earlier signing option. Signing depends on local key configuration and may prompt or fail; it does not make an unsigned object signed after the fact.

Done means

  • git write-tree produced the intended snapshot from the index.
  • git commit-tree printed a commit ID and git cat-file -t reports commit.
  • The parent list matches the history you intended.
  • A reference points at the commit only if you chose to create or move one.
  • You can remove a test reference with git update-ref -d, after checking its current value.