Why can't I push from a shallow clone?

GitWorkflowGit Clone

Git Problem Overview


The git clone --depth command option says

--depth <depth> 
Create a shallow clone with a history truncated to the specified number of revisions. 
A shallow repository has a number of limitations 
(you cannot clone or fetch from it, nor push from nor into it),
 but is adequate if you are only interested in the recent history of a large project with a long history,
 and would want to send in fixes as patches. 

Why do shallow clones have this limitation? Why is it a patches only workflow?

For some project workflows I need to pass just the latest commit from a single branch to a coder, and then have them be able to push their (fast forward) developments to the main server. This partly for security, IP protection and repo size, and partly to reduce the confusion that a big repo would bring to a naive coder. Is there a git workflow that allows this?


Update: Based on Karl Bielefeldt's answer the git checkout --orphan should be the right answer. But one still needs to 'clone' that branch alone to the new user, and be able to push it effectively.

The man page states: > git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>] --orphan > > Create a new orphan branch, named <new_branch>, started from > <start_point> and switch to it. The first commit made on this new > branch will have no parents and it will be the root of a new history > totally disconnected from all the other branches and commits. > > The index and the working tree are adjusted as if you had previously > run git checkout <start_point>. This allows you to start a new > history that records a set of paths similar to <start_point> by > easily running git commit -a to make the root commit. > > This can be useful when you want to publish the tree from a commit > without exposing its full history. You might want to do this to > publish an open source branch of a project whose current tree is > "clean", but whose full history contains proprietary or otherwise > encumbered bits of code. > > If you want to start a disconnected history that records a set of > paths that is totally different from the one of <start_point>, then > you should clear the index and the working tree right after creating > the orphan branch by running git rm -rf . from the top level of the > working tree. Afterwards you will be ready to prepare your new files, > repopulating the working tree, by copying them from elsewhere, > extracting a tarball, etc.

VonC's link to Junio's comments is interesting. I think the manual should provide the guidance in this case, and allow the right command [e.g. clone <branch> --options] to extract just the relevant part of the repo. Obviously the probability of push success is increased by having a few linked commits and SHA1s at the bottom of the history that will lock down the repo matching.


Update Git 1.9.0 : release notes 14 Feb '14.

"Fetching from a shallowly-cloned repository used to be forbidden, primarily because the codepaths involved were not carefully vetted and we did not bother supporting such usage. This release attempts to allow object transfer out of a shallowly-cloned repository in a more controlled way (i.e. the receiver becomes a shallow repository with a truncated history)."

This is good news for the shallow cloners. Next - Narrow clones possibly.

Git Solutions


Solution 1 - Git

As Junio C. Hamano (main Git maintainer) puts it:

> Isn't the rule more or less like:

> >If your shallow repository's history does not extend long enough and the other repository forked before your truncated history, wyou cannot compute the common ancestor and you cannot push out.

Update 2014: see "Is git clone --depth 1 (shallow clone) more useful than it makes out?": that limitation will be lifted with Git 1.9!

Update 2015: with Git 2.5+, you will even be able to fetch a single commit. See "Pull a specific commit from a remote git repository"


Original answer (August 2011):

> Actually, come to think of it, it is a lot stronger than "cannot compute the common".

> The history may look like this:

          R---R---R
         /
  --R---R---X---X---S---S---S

> where S are the commits you have in your shallow repository, and R are the commits that exist in the repository that receives your push.
Because your history is shallow, neither repository has 'X' that are the commits that need to exist in order to keep the history of recipient repository complete; the recipient is not shallow to begin with, and we do not want to make it shallow.

> If you cloned shallowly some time ago, worked without communicating with the other side while the other side progressed, AND if the other side's progress included a rewind & rebuild of the history, you would see a similar topology.
The leftmost 'S' in the above picture might have been the tip of the branch when you shallowly cloned with depth 1, and since then the remote end may have discarded topmost three commits and have rebuilt its history that leads to the rightmost 'R'.
In such a case pushing to the remote's HEAD will fail.


So it could work in some case, but it is not supported:

If I have to say something on this...

>- I think "is not supported" is a succinct way to give good enough information, but it would only work for intelligent people.

> - Not everybody is intelligent; some try it out themselves, see that the operation seems to work for their limited number of trials, and would conclude it would work most of the time.
And they congratulate their own intelligence for saying "most of the time", not "always".
And they get upset when they see it does not work, even though they have been warned.


For more on the shallow clone update process, see "How to update a git shallow clone?".

Solution 2 - Git

> Is there a git workflow that allows this?

Yes, it's to send in fixes as patches. git format-patch is specially designed to enable this. It's called a "gatekeeper" workflow, if you want to google for more details. It's hard to believe an organization as concerned with "security and IP protection" as yours isn't already using something similar, where one person or a small group is responsible for vetting "untrusted" changes before they make it into the real build.


Based on your comment, I now have a better idea of your requirements. What I would recommend is creating an orphan branch (see git checkout --orphan), from whichever point you want your devs to start. Clone only that branch to a different repository accessible to those devs, and let them clone, push, and pull normally from that repo.

Then when you need to reintegrate their changes to the official protected repository, just pull their branch, make a copy of it with git branch so you don't overwrite your original orphan (in case you want to repeat the process later), then rebase the copy onto your original branch point, and merge or whatever as normal. The history will look like they worked directly from your protected repo.

It's a little bit more complicated than normal, but that's the price paid for the extra isolation.

Solution 3 - Git

I found a workaround using git bundles.

This solution will replicate the exact same commits to the other repository like "git push" would do, and will not require rebasing or result in a changed commit ID.

It requires shell-access (such as ssh) to the target host, unfortunately.

I will show the solution by example.

First we need to get a shallow clone for demonstration purposes.

Lets fetch the single commit release v0.5.0.0 from https://github.com/skarnet/s6-rc into a new repository as a shallow clone.

I will use shell variables in my examples rather than including the example settings directly within the commands, because this will allow you to copy/paste the instruction from this posting directly into your shell, after setting the variables to different values which apply to your situation.

Therefore, feel free to replace the following variable assigmnents by using a different URL and release.

In the case of our example, the shallow clone can be created with:

$ url=https://github.com/skarnet/s6-rc
$ rel=v0.5.0.0
$ git init ${url##*/} && cd ${url##*/}
$ git pull --depth=1 "$url" $rel:master

This will create a "s6-src" (when using the above variable values) subdirectory containing the new clone.

Now that we have our shallow clone containing only a single commit with all its parent history missing in the local repository, we bundle this single commit into a git bundle file:

$ b=$rel.gbnd
$ git bundle create $b HEAD

This will create file v0.5.0.0.gbnd making use of the shell variables set before.

Now you have to copy this file to the target machine where you would normally like to push to. (Only that git push refuses to push from shallow clones and will therefore not work, at least not using older git versions.)

On the target host, do the following in order to create a new repository as a subdirectory, containing the same commit as bundled before:

$ url=https://github.com/skarnet/s6-rc
$ rel=v0.5.0.0
$ git init ${url##*/} && cd ${url##*/}
$ c=`git bundle unbundle $b | cut -d " " -f 1`; echo "$c"
$ git tag $rel $c # optional: create a tag for the imported commit.
$ git reset --hard $c
$ git fetch --depth=1 .

Note that you should set the variables to the same values as you did on the host from which the bundle was copied.

Also note that you can omit the "git init" it the repository already exists.

That's it!

However, the latter instructions only apply for regular checkouts.

Maybe you want to import a shallow clone bundle into a "bare" repository.

In this case, do the following instead:

$ url=https://github.com/skarnet/s6-rc
$ rel=v0.5.0.0
$ cd ${url##*/}.git
$ c=`git bundle unbundle $b | cut -d " " -f 1`; echo "$c"
$ git tag $rel $c
$ git fetch --depth=1 . $rel

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPhilip OakleyView Question on Stackoverflow
Solution 1 - GitVonCView Answer on Stackoverflow
Solution 2 - GitKarl BielefeldtView Answer on Stackoverflow
Solution 3 - GitGuenther BrunthalerView Answer on Stackoverflow