Pushing partial commits to remote in Git

If you have a huge commit history, you may want get to a smaller branch with short history and push it to a new remote for new developers. In this post, we show you how to do that.

The thought is simple, we would create a new base commit that will have no parents and make the commits that we want them in the new branch to rebase onto it, thus we get a branch with less commits and we then push it to remote. Like a shallow clone, if an user clones the repository with less history from that remote and want more history later, it is also possible to do that.

We shall show you how to do that with a full example. The example comes from git book: replace, here we make the process more clearer for you.

Create a short branch and push it to the remote

Assuming we have a repository with below 5 commits and we want a branch named short-history that holding just the 4th and the 5th commit.

$ git log --oneline
* eee5b39 (HEAD -> master) 5th commit
* 0507fc9 4th commit
* d38324c 3rd commit
* 0f302d0 2nd commit
* 0d74a45 1st commit

Step 1. Create new branch to push

# Create and checkout a new branch
$ git checkbout -b short-history

# Now the repo looks like:
$ git log --oneline --decorate
* eee5b39 (HEAD -> master, short-history) 5th commit
* 0507fc9 4th commit
* d38324c 3rd commit
* 0f302d0 2nd commit
* 0d74a45 1st commit

Step 2. Create new base commit for the short branch

In this step, we create a new base commit for the 4th and 5th commit. It is useful to create a base commit that contains information of the old parent d38324c (the 3rd commit). Therefore we use that commit’s top-level tree to create the base commit (a commit normally contains a top-level tree, the parent commits if any and the committer information).

$ echo 'new base commit with tree from d38324c' | git commit-tree d38324c^{tree}
dab34243e0301c42176e5c8895c0acee1397a1e2

commit-tree is a low-level git command to create a commit object. Here we pass a tree SHA-1 to it and a commit message to create a commit that will have no parents.

Step 3. Rebase the branch to the new base commit

# Rebase to the new base commit
$ git rebase --onto dab3424 d38324c
Successfully rebased and updated refs/heads/short-history.

# Now the repo looks like:
$ git log --oneline --decorate
* 0b4d7b4 (HEAD -> short-history) 5th commit
* 2172414 4th commit
* dab3424 new base commit with tree from d38324c
* eee5b39 (master) 5th commit
* 0507fc9 4th commit
* d38324c 3rd commit
* 0f302d0 2nd commit
* 0d74a45 1st commit

Now we’ve re-written the short branch that only contains a base commit dab3424 and the 4th and 5th commit as we wanted. The old branch master remasters as it was.

4. Push the short branch to remote

Now we are ready to push the short branch short-history to a new remote.

# Push the new branch to a new remote
$ git remote add short-remote <short-remote-url>
$ git push short-remote short-history:master

If we clone from that remote, we will get 3 commits as expected:

$ git clone <short-remote-url>
$ cd <repo-folder>
$ git log --oneline master
0b4d7b4 (HEAD -> short-history) 5th commit
2172414 4th commit
dab3424 new base commit with tree from d38324c

Fetch more history later to the short history

If we want more history later in the repository that we just cloned, we can fetch from a remote with old commit history and use git replace to combine them. git replace is a command can be used to replace a commit with another one to rebuild the entire history.

Suppose the remote repository with the old history contains the 1st, 2nd, 3rd and 4th commit (Yes, this is also a way to split a huge repository to two repos that one with the old history and one with the new history). Below is how to do that.

# Add remote that has the old history
$ git remote add long-remote <long-remote-url>

# View the commits in the old history
$ git log --oneline long-remote/master
* 0507fc9 4th commit
* d38324c 3rd commit
* 0f302d0 2nd commit
* 0d74a45 1st commit

# Replace the 4th commit in the short history with the 4th commit in the old history
# to combine them.
$ git replace 2172414 0507fc9

# Now the branch looks like:
$ git log --oneline master
* 0b4d7b4 5th commit
* 2172414 4th commit
* d38324c 3rd commit
* 0f302d0 2nd commit
* 0d74a45 1st commit

Even it shows 2172414 for the 4th commit, it is actually 0507fc9 that we replace it with.

Resourcess

Pushing to a non-bare Git repository

By default, pushing a branch to a non-bare Git repository in which the branch is currently checked out is denied. The reason is it will cause inconsistent issue between the working tree (also the index) and the pushed HEAD. Through setting receive.denyCurrentBranch option, you can able to do such a push.

Errors when pushing the current branch to a non-bare Git repository

$ git push master
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 3.63 KiB | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: error: is denied, because it will make the index and work tree inconsistent
remote: error: with what you pushed, and will require 'git reset --hard' to match
remote: error: the work tree to HEAD.
remote: error:
remote: error: You can set 'receive.denyCurrentBranch' configuration variable to
remote: error: 'ignore' or 'warn' in the remote repository to allow pushing into
remote: error: its current branch; however, this is not recommended unless you
remote: error: arranged to update its work tree to match what you pushed in some
remote: error: other way.
...

receive.denyCurrentBranch

The available values for receive.denyCurrentBranch:

  • refuse or true, refuse the push. It is the default value.
  • ignore or false, allow the push.
  • warn, allow the push but give a warning message to the client.
  • updateInstead, allow the push and update the current branch in the remote repository.

The last three values allow the push, but ignore and warn will cause inconsistent issues. You need to execute git reset --hard manually later to make the index and working directory match the pushed HEAD in the remote repository. The effect of updateInstead looks like it allows the push and executes git reset --hard later for you.

Config

Set receive.denyCurrentBranch to ignore, warn or updateInstead (according to your needs) in the remote non-bare repository to allow pushing the current branch.

# Config receive.denyCurrentBranch to ignore in the remote repository
# The configuration is applied to only this respository.
$ git config receive.denyCurrentBranch ignore

# Config receive.denyCurrentBranch to updateInstead in the remote repository
$ git config receive.denyCurrentBranch updateInstead

Deleting a local or remote branch in Git

Use git branch -d to delete a local branch. Use git push --delete to delete a branch on the remote repository.

Delete a local branch

Make sure you are not on the branch to be deleted :

# Delete local branch 'test'

# First switch to another branch if you are on 'test' branch
$ git checkout master

# Do delete action
$ git branch -d test

If your branch has not been merged, use -D option to force delete it :

$ git branch -d test
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D test'.

$ git branch -D test
Deleted branch test (was 4f10b4a).

Delete a remote branch

Use --delete option to push command :

# Delete a remote branch 'test' on Git version above v1.7.0
$ git push origin --delete test

The command above is available after Git v1.7.0. If you are using an earlier version :

# Push empty source to remote branch of 'test', meaning deleting it
$ git push origin :test