54

Here's the situation:

I have a public repository for my open-source app on github.com. However, now I'd like to write some specific code that will not be public (I might use it in a commercial version of my application).

I figured I could use the same repository, and I'd create a "private" branch in my git repository that I wouldn't push.

But, mistakes happen. Is there some way to forbid git from ever pushing a branch to remote servers?

If there's a better way to handle this situation, I would of course welcome any suggestions.

houbysoft
  • 32,532
  • 24
  • 103
  • 156

7 Answers7

62

Here's how the pre-push hook approach works, with a branch called dontpushthis.

Create this file as .git/hooks/pre-push:

#!/usr/bin/bash
if [[ `grep 'dontpushthis'` ]]; then 
  echo "You really don't want to push this branch. Aborting."
  exit 1
fi

This works because the list of refs being pushed is passed on standard input. So this will also catch git push --all.

Make it executable.

Do this in every local repository.

When you try to push to that branch, you'll see:

$ git checkout dontpushthis
$ git push
You really don't want to push this branch. Aborting.
error: failed to push some refs to 'https://github.com/stevage/test.git'

Obviously this is as simple as it looks, and only prevents pushing the branch named "dontpushthis". So it's useful if you're trying to avoid directly pushing to an important branch, such as master.

If you're trying to solve the problem of preventing confidential information leaking, it might not be sufficient. For example, if you created a sub-branch from dontpushthis, that branch would not be detected. You'd need more sophisticated detection - you could look to see whether any of the commits on the "dontpushthis" branch were present on the current branch, for instance.

A safer solution

Looking at the question again, I think a better solution in this case would be:

  1. Have one repo which is public
  2. Clone that to a new working directory which is private
  3. Remove the remote (git remote rm origin) from that working directory.
  4. To merge public changes, just do git pull https://github.com/myproj/mypublicrepo

This way, the private repo working directory never has anywhere it could push to. You essentially have a one-way valve of public information to private, but not back.

wamae
  • 660
  • 9
  • 21
Steve Bennett
  • 114,604
  • 39
  • 168
  • 219
  • 2
    this is what I'm using ``` #!/bin/sh if [[ `grep -e develop -e master` ]]; then echo "Please do no push directly to develop or master" exit 1 fi ``` – Jacob Evans Jun 24 '16 at 17:14
  • 2
    The safer solution is much better. Thanks! – Julio Saito Sep 30 '16 at 04:41
  • 7
    I also like the safer solution way more! Instead of 3. I would do a ``git remote set-url --push origin http://no-push-to-this-remote:99999/`` This sets only the push URL of that remote to a invalid url (Port 99.999 is invalid), so a regular fetch/pull is possible, just pushes to this remote will fail with an immediate error message. This is a bit more convenient then to always have to provide the URL for fetching. – Stefan Jun 25 '19 at 09:31
29

A slightly hackish solution: Make a dummy branch on GitHub with the same name as your real branch, and make sure it would not be a fast forward merge. That way, the push operation will fail.

Here's an example.

$ git clone git@github.com:user/repo.git
$ cd repo
$ git checkout -b secret
$ echo "This is just a dummy to prevent fast-forward merges" > dummy.txt
$ git add .
$ git commit -m "Dummy"
$ git push origin secret

Now that the dummy branch is set up, we can recreate it locally to diverge from the one on GitHub.

$ git checkout master
$ git branch -D secret
$ git checkout -b secret
$ echo "This diverges from the GitHub branch" > new-stuff.txt
$ git add .
$ git commit -m "New stuff"

Now if we accidentally try to push, it will fail with a non-fast forward merge error:

$ git push origin secret
To git@github.com:user/repo.git
! [rejected]        secret -> secret (non-fast forward)
error: failed to push some refs to ‘git@github.com:user/repo.git’
hammar
  • 138,522
  • 17
  • 304
  • 385
  • 1
    Sorry, could you please explain more what you mean by "make sure it would not be a fast-forward merge"? Thanks – houbysoft Jul 11 '11 at 01:58
  • 2
    Push a commit on your dummy branch that's not gonna be on your real branch so that they diverge. – hammar Jul 11 '11 at 06:01
  • Nice! This would work if you had both a public (origin) and a private repo (private) too. Just put the dummy branch only on the public repo. Then git push private secret would work but git push origin secret wouldn't. – Benjamin Atkin Aug 01 '11 at 20:09
  • 1
    A less-hackish method is to write a `pre-push` hook and include it in your local repo, but that script would need to be placed in every repo with both access to the branch and the ability to push it. – Dan Hunsaker Jan 19 '15 at 22:57
  • 2
    IMHO, pre-push is better. This solution would not work for those who like me that 'git push -f'. I have a public small repository sharing my dot files. Therefore it is not a big deal to 'git push -f' (I rebase/reset etc. to make the history clean and neat :( ). But I really need to ensure the private branch which contains my confidential info is never pushed to remote. – Lungang Fang Nov 11 '15 at 01:05
  • 1
    Even easier for GitHub, create the branch on GitHub and enable branch protection on it and require pull request reviews or status checks (and be sure to include administrators). Then GitHub will not allow you to push the branch, even with `-f`. – asmeurer Jul 27 '18 at 21:10
10

A touch up of the .git/hooks/pre-push script from @steve-bennett

    #!/usr/bin/bash

    branch_blocked=mine

    if grep -q "$branch_blocked"; then
        echo "Branch '$branch_blocked' is blocked by yourself." >&2
        exit 1
    fi
Lungang Fang
  • 1,427
  • 12
  • 14
6

Why not simply use the example of pre-push provided with current git version?

The idea is to begin the commit message of the first commit of your private branch with the word PRIVATE:.

After setting the pre-push script, for every push it checks the commit messages of all the log of pushed refs. If they start with PRIVATE:, the push will be blocked.

Here are the steps:

  • Create a file in .git/hooks/pre-push
  • Give it execution rights
  • Past the following script in it

    #!/bin/sh
    
    remote="$1"
    url="$2"
    
    z40=0000000000000000000000000000000000000000
    
    IFS=' '
    while read local_ref local_sha remote_ref remote_sha
    do
            if [ "$local_sha" = $z40 ]
            then
                    # Handle delete
                    :
            else
                    if [ "$remote_sha" = $z40 ]
                    then
                            # New branch, examine all commits
                            range="$local_sha"
                    else
                            # Update to existing branch, examine new commits
                            range="$remote_sha..$local_sha"
                    fi
    
                    # Check for WIP commit
                    commit=`git rev-list -n 1 --grep '^PRIVATE:' "$range"`
                    if [ -n "$commit" ]
                    then
                            echo "Error: Found PRIVATE commit in $local_ref."
                            echo "The commit is in the range $range."
                            echo "NOT pushing!"
                            exit 1
                    fi
            fi
    done
    
    exit 0
    
    remote="$1"
    url="$2"
    

Example of failure

$ git push origin private/old-kalman-filter 
Found PRIVATE commit in refs/heads/myforbiddenbranch, the commit is in the range 
a15c7948676af80c95b96430e4240d53ff783455. NOT PUSHING!
error: failed to push some refs to 'remote:/path/to/remote/repo'

To make the branch pushable again, you can either remove the hook or, better, modify the commit message to remove the forbidden word.

This script can be modified to only consider one forbidden remote by checking remote_ref. But in that case, do not forget to copy this hook in all the repos allowed to receive this branch.

Aquadarius
  • 510
  • 5
  • 5
  • 1
    This is very straightforward. I also added a check on the name of the remote, so that if a remote name starts with the string "private" the push will go on without further checks. In this way, I can have a private remote (e.g. for backup reasons) where I can push everything, and a public one with the common stuff and no private parts. Thanks! – polettix Jul 26 '16 at 15:10
2

If you use GitHub, you can create a branch on GitHub with the same name as your branch. There is no need to push any commits to it, just make an empty branch off master or whatever (you can do it in the GitHub interface by typing the branch in the "branch" popdown and clicking create branch <branch-name>).

Then, go to the branch settings for the repository (e.g., https://github.com/<user>/<repo>/settings/branches/<branch-name>) and enable branch protection for your branch. Be sure to check all the boxes, particularly, the require reviews or status checks boxes, which disallows the branch from being pushed to directly (it would have to be pushed from a pull request), and also be sure to check the box to include administrators.

Then GitHub will not allow you to push to the branch, even if you use -f. You'll get a message like

$ git push -f origin private
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 981 bytes | 981.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
remote: error: GH006: Protected branch update failed for refs/heads/private.
remote: error: At least 1 approving review is required by reviewers with write access.
To github.com:<user>/<repo>.git
 ! [remote rejected] private -> private (protected branch hook declined)
error: failed to push some refs to 'git@github.com:<user>/<repo>.git'
asmeurer
  • 86,894
  • 26
  • 169
  • 240
2

There are multiple solutions:

  1. Non technical, just adjust the license to a commercial one for your branch
  2. Make a private repository on github which contains your fork
  3. Make a git-hook on the server (afaik not possible with github)
  4. Write an wrapper for git-push to prevent the push with git push
Ulrich Dangel
  • 4,515
  • 3
  • 22
  • 30
2

You can create a branch that does not exist in your remote repository.

That way if you just do:

git push origin

it will push only branches that exist on the remote repository.

Also look into .git/config (within the local repository directory) file after creating the branch - you will see that every local branch can have different remote repository assigned. You can take advantage of that by assigning this branch to separate (private) repository, but the is not the universal solution (the branch still can be pushed to origin remote, if explicitly ordered to, or by command git push origin).

Tadeck
  • 132,510
  • 28
  • 152
  • 198
  • @manojlds: Technically there are exactly two if's ;) Seriously, please explain what did you mean. – Tadeck Jul 11 '11 at 00:21
  • Usually I, and many I know, do `git push remote branch` - that invalidates both of your if's – manojlds Jul 11 '11 at 00:22
  • @manojlds: If you order git to upload your private branch to your private repository (`git push origin my_private_branch`), why do you think git should not do it? And actually most people I know are using just `git push` (or `git push [remote]`), if there are multiple branches, remotes and writing `git push origin my_private_branch` seems to be a lot less efficient to be written 100 times a day. – Tadeck Jul 11 '11 at 00:27
  • `why do you think git should not do it?` - that is the point of this question. You may be right on the second part. – manojlds Jul 11 '11 at 00:29