Why can't I push this up-to-date Git subtree?

I found the answer on this blog comment https://coderwall.com/p/ssxp5q

If you come across the "Updates were rejected because the tip of your current branch is behind. Merge the remote changes (e.g. 'git pull')" problem when you're pushing (due to whatever reason, esp screwing about with git history) then you'll need to nest git commands so that you can force a push to heroku. e.g, given the above example:

git push heroku `git subtree split --prefix pythonapp master`:master --force

On windows the nested command doesn't work:

git push heroku `git subtree split --prefix pythonapp master`:master --force

You can just run the nested bit first:

git subtree split --prefix pythonapp master

This will (after a lot of numbers) return a token, e.g.

157a66d050d7a6188f243243264c765f18bc85fb956

Use this in the containing command, e.g:

git push heroku 157a66d050d7a6188f243243264c765f18bc85fb956:master --force

Use the --onto flag:

# DOESN'T WORK: git subtree push --prefix=public/shared project-shared master --onto=project-shared/master

[EDIT: unfortunately subtree push doesn't forward --onto to the underlying split, so the operation has to be done in two commands! With that done, I see that my commands are identical to those in one of the other answers, but the explanation is different so I'll leave it here anyway.]

git push project-shared $(git subtree split --prefix=public/shared --onto=project-shared/master):master

Or if you're not using bash:

git subtree split --prefix=public/shared --onto=project-shared/master
# This will print an ID, say 0123456789abcdef0123456789abcdef,
# which you then use as follows:
git push project-shared 01234567:master

I spent hours poring through the git-subtree source to figure this one out, so I hope you appreciate it ;)

subtree push starts by running subtree split, which rewrites your commit history into a format which should be ready to push. The way it does this is, it strips public/shared/ off the front of any path which has it, and removes any information about files that don't have it. That means even if you pull non-squashed, all the upstream sub-repository commits are disregarded since they name the files by their bare paths. (Commits that don't touch any files under public/shared/, or merge commits that are identical to a parent, are also collapsed. [EDIT: Also, I've since found some squash detection, so now I'm thinking it's only if you pulled non-squashed, and then the simplistic merge commit collapsing described in yet another answer manages to choose the non-squashed path and discard the squashed path.]) The upshot is, the stuff it tries to push ends up containing any work someone committed to the current host repository you're pushing from, but not work people committed directly to the sub-repository or via another host repository.

However, if you use --onto, then all the upstream commits are recorded as OK to use verbatim, so when the rewriting process comes across them as one of the parents of a merge it wants to rewrite, it will keep them instead of trying to rewrite them in the usual way.