1

Every single article on the Internet on "force pushing a Git Subtree" uses the gh-pages:gh-pages example, like Git force push subtree: error: unknown option `force', etc.

However, that gh-pages:gh-pages means

force the push of the gh-pages branch to the remote gh-pages branch at origin

and I simply cannot apply to my situation.

I have a single master branch, and I've done

$ git subtree split --prefix=my/subtree -b for-force-push
Created branch 'for-force-push'
1044de7a3abdccd0992c6973d23e0faabcc0082c

The next line should be:
git push -f origin master:for-force-push
right?

However, I saw:

. . .
remote: Create a pull request for 'for-force-push' on GitHub by visiting:
. . .
remote: Create pull request for for-force-push:
remote:   https://bitbucket.org/. . .
. . .

I.e., every single of my remote tell me to Create pull request, except my subtree, which sees no update. It is a gist repo BTW.

Is I'm doing something wrong or just subtree of gist repo cannot be force-pushed?

PS.

Found another "shortcut" of

git push origin `git subtree split --prefix=my/subtree master`:master --force

but that simply put my git in the status of:

On branch master
Your branch and 'origin/master' have diverged,
and have 140 and 7 different commits each, respectively.

which is a total disaster :(.

UPDATE:

I tried to create a minimum set of steps to dup the situation, and I've exhausted everything possible I could think of, but still unable to do the force push to my gist subtree. Here are the details:

cd /tmp
mkdir sbtr
cd sbtr

git init
git commit --allow-empty -n -m "Initial commit."

git remote add -f gist-subtree git@gist.github.com:7...36a.git

git subtree add --prefix=sbtrpth gist-subtree master --squash
git subtree pull --prefix=sbtrpth gist-subtree master --squash

# Create a situation that Updates were rejected because the tip of your current branch is behind its remote counterpart. 
# Then, all the following attempts failed:

 git push gist-subtree `git subtree split --prefix=sbtrpth gist-subtree master`:master --force
 git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force
 git remote add origin git@gitlab.com:me/sbtr.git
 git push origin `git subtree split --prefix=sbtrpth master`:master --force

Here is the detailed log of above errors:

$ git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force
fatal: ambiguous argument 'gist-subtree': unknown revision or path not in the working tree.

$ git push gist-subtree `git subtree split --prefix=sbtrpth master`:master --force

error: The destination you provided is not a full refname (i.e.,

starting with "refs/"). We tried to guess what you meant by:


- Looking for a ref that matches 'master' on the remote side.

- Checking if the <src> being pushed ('7...a28')

  is a ref in "refs/{heads,tags}/". If so we add a corresponding

  refs/{heads,tags}/ prefix on the remote side.


Neither worked, so we gave up. You must fully qualify the ref.

hint: The <src> part of the refspec is a commit object.

hint: Did you mean to create a new branch by pushing to

hint: '7...a28:refs/heads/master'?

error: failed to push some refs to 'gist.github.com:7...36a.git'


$ git push origin `git subtree split --prefix=sbtrpth master`:master --force

Warning: Permanently added the ECDSA host key for IP address '172.65.251.78' to the list of known hosts.

error: The destination you provided is not a full refname (i.e.,

starting with "refs/"). We tried to guess what you meant by:
xpt
  • 20,363
  • 37
  • 127
  • 216
  • Found another similar complain on the Internet -- _"it would be nice if the local and remote branches weren't named the same in your example - I can never remember what order gh-pages:gh-pages is :)"_ – xpt Nov 06 '21 at 03:35
  • Is this the same as https://stackoverflow.com/questions/33172857/how-do-i-force-a-subtree-push-to-overwrite-remote-changes? If not, what are the specific differences? – Potherca May 12 '22 at 14:12
  • If you think https://stackoverflow.com/questions/33172857/ is the answer to my question @Potherca, then please tell me what the `dist` and `production` _there_ should be substituted as/into _here_ in my above case, ans explain why my `git push origin ```git subtree split --prefix=sbtrpth master```:master --force` in OP is not working as expected. thx. – xpt May 12 '22 at 20:48
  • I did some experiments (which can be found at https://gist.github.com/Potherca/7b60012d6b03d05543ac8381e9462fc1) and the `git push $(git subtree split)` combination _seems_ to work just fine... Could you check where my scenario differs from yours? (Other than mine not using `--squash`, which shouldn't matter...) – Potherca Jun 03 '22 at 14:11
  • Regarding "The next line should be", you need to use the `ref`, so the line _should be: `git push --force gist-subtree 1044de7a3abdccd0992c6973d23e0faabcc0082c:master` – Potherca Jun 03 '22 at 14:13

1 Answers1

1

TL;DR

Your refspec is backwards: you wanted git push -f origin for-force-push:master. However, that would get you the same thing you got with the one you called a "total disaster". I don't know what you really intend here and cannot give any further advice on that part.

Long

Both git fetch and git push use a refspec—well, one or more, but let's just describe one, first. The format of a refspec is +<src>:<dst> with the + being optional—it sets the force flag—and the src and/or dst being optional as well. If you drop some part(s)—source and/or destination—things get a little confusing, and if you use unqualified references for source and/or destination, things get a little confusing. The interaction of + with git push --force or git push --force-with-lease is confusing as well. So it's best, for discussion purposes, to start with these assumptions:

  • The force flag, if present, is present as +, and if not, is just absent.

  • The source and destination fields are both filled in. Both are fully-qualified.

This removes all the confusers except for what the source and destination mean, so now we can explain those simply enough. Remember that git fetch means get commits (and/or other internal Git objects, but mostly commits) from them and git push means send commits (and/or other internal Git objects) to them. Them is, by definition, some other Git repository, being acted on by various git something commands run on another machine1.

If we're sending stuff to them, the source would be commits (and other Git objects) in our repository. We find these commits using our names, e.g., our branch names: refs/heads/for-force-push for instance. The destination is a name in their repository: refs/heads/master for instance. So, since the syntax is <src>:<dst>, we'll use:

git push <them> refs/heads/for-force-push:master

If we're getting stuff from themgit fetch; the badly-named git pull does too much,2 so we'll ignore it here—then they are the "source" and we are the "destination", and we would use:

git fetch <them> refs/heads/master:refs/remotes/origin/master

for instance.

If you provide multiple refspecs, such as:

git push origin <spec1> <spec2>

Git will operate on all the refspecs, sending multiple update requests for instance. The push operation first sends any commits and/or other internal objects required, and then ends by asking (regular push) or commanding (force-push) the other Git to create or update various names—references—in their repository. The values their Git should stuff into these new or updated references are the values your Git found by resolving the source parts, which also determined which commits and/or other objects had to be sent. Hence:

git push origin refs/heads/br1:refs/heads/br1 refs/heads/br2:refs/heads/br2

sends the commits we have on our branch br1 and the commits we have on our branch br2, and then asks (politely) that their Git should set their branch name br1 to match our branch name br1, and to set their branch name br2 to match our branch name br2.

This lets you push more than one branch at a time, which is something a lot of people don't seem to know. It's important though, in pre-push (sender side) and pre- and post-receive hooks (receiving side) hooks: you must read through all the updates that are being requested or commanded.


1In a degenerate case, these commands might be run on your own machine: for instance, you can push or fetch from your laptop, to your laptop. You can do this over ssh and/or https if you have an ssh server and/or https server running on your laptop. This is sometimes how we set up a VM, using Docker or VirtualBox or whatever, for instance. The exact details depend a lot on the software you're using. But mostly we're sending from laptop to GitHub or whatever, and it's easier to think about this if "their" Git software is running on some server like GitHub, and "their" Git repository is a completely separate repository over on GitHub.

2If what git fetch does had been called git pull, we'd say "pull and merge" or "pull and rebase" and it would all make more sense. Instead, we say "fetch and merge-or-rebase" by saying git pull, and then we have to stop and walk things back a bit and figure out whether we're merging or rebasing before we can move forward again. As with revert-vs-backout, Mercurial got this one right and Git got this one wrong, though the revert-vs-backout situation is pretty clearly worse.


Abbreviating

Writing out refs/heads/master, refs/remotes/origin/master, and so on is kind of a pain. Can't Git figure this out? If I want to push branch br1, why can't I use:

git push origin br1:br1

for instance? Git would just have to fill in refs/heads on both sides for me. If I:

git push origin v1.7:v1.7

can't Git figure out that v1.7 is a tag, and fill in refs/tags/ on both sides for me?

The answer here is: yes, Git can figure this out. The way it figures this out is complicated though. Someone needs to have the name. For git push, we're sending—we're the source—so we need to have the name, so that Git can look up the commit hash ID. But when we use git push we're not even required to put a name on the left as the source:

git push origin a123456:refs/heads/newbranch

is allowed if a123456 is the (abbreviated) hash ID of one of our commits, and:

git push origin HEAD:refs/heads/newbranch

is also allowed even if we're on a detached HEAD. So if the left (source) side of a git push refspec isn't fully qualified, the right side can be used to qualify the name—but now the destination name has to match some existing name in the other Git. Which one will it match? You might not know. It's a bad idea to let Git match on its own, as it might match the wrong thing (a tag name for instance). I recommend using a fully-qualified reference here if you aren't using your own Git to do the resolving via the source. If you're trying to create a new branch from a detached HEAD—which is one of my own use cases for this—you must provide a fully qualified name anyway:

$ git push origin $(git rev-parse HEAD):xyzzy  # simulate detached HEAD
error: The destination you provided is not a full refname (i.e.,
starting with "refs/"). We tried to guess what you meant by:

- Looking for a ref that matches 'xyzzy' on the remote side.
- Checking if the <src> being pushed ('11ae6ca18f6325c858f1e3ea2b7e6a045666336d')
  is a ref in "refs/{heads,tags}/". If so we add a corresponding
  refs/{heads,tags}/ prefix on the remote side.

Neither worked, so we gave up. You must fully qualify the ref.
hint: The <src> part of the refspec is a commit object.
hint: Did you mean to create a new branch by pushing to
hint: '11ae6ca18f6325c858f1e3ea2b7e6a045666336d:refs/heads/xyzzy'?

The git fetch command is different from git push, in part for historical reasons. When we use git push, we must send the other Git some name(s) to set. That is, git push origin master: makes no sense. So this form of git push is simply invalid. However, git push origin :master "means" git push --delete origin master: that is, we have our Git ask their Git to delete their name master. As before, since we didn't look up a name on our side, we rely on their Git's names to figure out whether this is a branch name or a tag name. It's not entirely wise to omit the refs/heads/ or refs/tags/ part here either—their names may change before our Git gets to look them up during the git push—but at least this time you probably expect to match exactly one of those two kinds of names on the destination (i.e., we're not trying to create a new branch or tag, we're trying to delete an existing branch or tag).

So, during git push, we can:

  • abbreviate names that we actually have one of, because we know our Git will look up our name and find the fully qualified version;
  • omit the :dst part entirely, because git push origin br1 means git push origin br1:br1 anyway.

This lets us push commits to the other Git and use the same branch name on both sides, which is probably our most common use case. And, if we've set origin/br1 as the upstream of the current branch br1, we can just run git push and be done with it—provided we haven't fiddled with the push.default setting, anyway.3

The special syntax:

git push origin :

invokes the matching mode. Here, our Git calls up their Git and has them list out their branch names. Now we know if they have a branch named br1, if they have a branch named master, and so on. Our Git also knows, because it's looking right at our repository, whether we have branches named br1 and master and so on. For all matching branch names, our Git tries to git push that pair of names.


3This works with the default-since-Git-2.0 push.default of simple, and also with current and upstream. It does something different with matching, which is the default in some ancient versions of Git; if you're using those, either upgrade, or stick with git push origin br1. If you have set your push.default to nothing, Git will force you to spell it out: I tried this mode for a while but it was too painful / tiring, and I went back to simple for most of my usage.


git fetch is different

When we run git push with no arguments or just one argument such as a remote name (git push origin), the default, in modern Git, is:

  • find the upstream setting of the current branch;
  • make sure it's origin/B where B is the name of the current branch (and replace origin here with the appropriate remote name as needed), and require that branch B exist over on the other Git;
  • do a git push origin B:B (again replace origin here if appropriate).

So this pushes one set of commits, from the current branch, and asks the other Git to update one of its branches: the one whose name is in the current branch. If you find the first two bullet points here annoying—the requirement that branch B exist on the remote, and be set as the upstream of the current branch—you can change your push.default to current. Note that this makes it easy to accidentally expose a private branch that you did not mean ever to push, though, so be careful if you do this. (The matching mode doesn't have this problem, so if you like the behavior Git had in 1.x, you can change your push.default to matching; just be aware that this may often push multiple branches.)

But git fetch is different. If we run git fetch with no arguments, Git will:

  • find the upstream of the current branch, if set, and get the name of the remote from there: if we're on branch paradise and its upstream is phloston/paradise, the remote that git fetch will use will be phloston; if that fails, fall back to origin;
  • look up remote.remote.fetch, which is a multi-valued configuration option;
  • use those refspecs, in those configuration settings (plural), as the refspecs for the fetch.

If we provide one or more refspecs on the command line—as in:

git fetch origin master

for instance—then git fetch uses the provided remote (we have to give one4) and refspec(s). Those refspecs control what happens—well, mostly. We'll come back to "mostly" in a bit.

If we leave them out, with git fetch or git fetch origin, though, we get the default from remote.origin.fetch (or whatever other setting). This default is responsible for a lot of mysteries in Git. In particular, this is how single-branch clones work.

If we make a single-branch clone (of some URL) with, e.g.:

git clone -b somebranch --single-branch <url>

the remote.origin.fetch setting in this clone will be:

+refs/heads/somebranch:refs/remotes/origin/somebranch

If we don't use --single-branch (nor --depth, which sets --single-branch), we get instead:

+refs/heads/*:refs/remotes/origin/*

Note that these refspecs are all two-parters: there's a src and a dst, separated by a colon :, and with the leading + force-flag always set. The destination is a remote-tracking name, in the refs/remotes/origin/ names in this case. The source is a branch name. So this is why our Git copies their branch names to our remote-tracking names. Their master or main becomes our origin/master or origin/main. Their dev becomes our origin/dev. If they have an origin/whatever, it's refs/remotes/origin/whatever in their repository, so we don't copy that at all.

This tells us something else about refspecs: they can contain wildcard characters. We mostly only use these with git fetch, and even then only with the default fetch values. But it is possible to use them elsewhere (feel free to experiment with this, but be careful, it's easy to make a mess: do these experiments with copies of repositories, or with junk temporary ones, not ones you want to keep). Be mindful and careful of the difference between shell * expansion and Git * expansion.

These default refspecs, for git fetch without a refspec on the command line, are only used if you don't put a refspec on the command line. Or are they? Now we come to another difference between fetch and push.

In git push, a request of the form git push remote src:—a source without a destination—is invalid:

$ git push origin branch:
fatal: invalid refspec 'branch:'

(even if branch is a valid branch, as it is in the test repository I used here). But with git fetch, it's not an error:

$ git fetch origin branch:
From: <url>
 * branch            branch     -> FETCH_HEAD

There's this funky FETCH_HEAD in the output here. Now watch what happens when I delete origin/branch and then re-run this same git fetch:

$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
$ git fetch origin branch:
From <url>
 * branch            branch     -> FETCH_HEAD
 * [new branch]      branch     -> origin/branch

The same things happen if I use branch, without a colon. This is where the "mostly" part breaks down. If I repeat the git branch -r -d to delete origin/branch, and then fetch directly from the URL, without using the name origin:

$ git branch -r -d origin/branch
Deleted remote-tracking branch origin/branch (was 222c4dd).
git fetch <url> branch
From <url>
 * branch            branch     -> FETCH_HEAD

This time, there was no:

 * [new branch]      branch     -> origin/branch

line. What's going on here? It's time for another answer section.


4Technically, we have to provide a positional argument but it need not be a remote. If it's not a remote, all the usual rules about remotes go out the window here. The obvious substitute rules apply: a refspec is required, and there's no remote to get a default refspec, so you must give at least one; if you did give one or more, the remote.remote.fetch lines would have been ignored, so the fact that we can't get them is irrelevant, except for the "mostly" note above.


Historical raisins (aka hysterical raisins or hysterical reasons)

In very old Git, remotes (like origin) did not exist. One had to fetch directly from a URL every time. This was a pain, so Git grew several different ways to deal with it, which eventually resulted in the invention of remotes, and the standard first remote, origin, that git clone makes for us.

Without remotes, though, there could never be any remote-tracking names in the first place. If we don't have origin, how can we have origin/branch? The answer is: we didn't. Instead, git fetch just wrote its information to a file, .git/FETCH_HEAD:

$ cat .git/FETCH_HEAD 
222c4dd303570d096f0346c3cd1dff6ea2c84f83        branch 'branch' of <url>

The exact format of what goes into this file is a bit complicated, but when git pull was a shell script, git pull depended heavily on it: git pull ran git fetch, then used the hash IDs (seen on the left), the not-for-merge that's not included here to skip some lines if necessary, and the information on the right to build git merge commands. So this would allow git pull url branch to run:

git merge -m "merge branch 'branch' of <url>" <hash>

(which git pull still does today, except now it's not a shell script any more and does not need the FETCH_HEAD file left between running git fetch and the subsequent git merge as both fetch and merge steps are built into the C program).

With the invention of remotes, this could be simplified, but for a long time it wasn't: git fetch still wrote .git/FETCH_HEAD and git pull was still a shell script that ran git fetch and then ran git merge after grepping out the right line(s) and building the command line arguments. Even today, for compatibility, git fetch still writes this FETCH_HEAD file.

Because of that, git fetch can do a fetch from a URL or remote with a refspec that consists only of a source part. The obvious thing for git fetch to do in this case is to write only the FETCH_HEAD file: the information is there, if we need it.

But ... this is not convenient. So with the invention of remotes, and the remote.origin.fetch configuration line(s), git fetch was told to read those lines and obey those refspecs by default. This will create remote-tracking names, which are much more convenient: you just run git fetch or git fetch origin and now you have origin/branch as appropriate. Since the default setup uses the force flag, your origin/branch is always updated to match their branch.5

So git fetch or git fetch origin does wonderful things: it completely updates all of our remote-tracking names with any new commits that have appeared on the other Git repository, based on their current branch names.6 We now know everything there is to know about the state of their repository, at least as of the nanosecond that our fetch ran. (By now, seconds may have passed and things could be wildly different, depending on how active their repository is.)

But what if we run, say, git fetch origin master or git fetch origin main? Now we're asking to update only the refspec master. In git push, master was short for master:master, which turned into refs/heads/master:refs/heads/master. But for git fetch, master is short for master: or refs/heads/master:. This writes to .git/FETCH_HEAD and then stops.

Well, it did that until Git version 1.8.4, that is. Up until that point, git fetch origin master did not update our remote-tracking name origin/master. But this was ... sub-optimal? Icky? I, for one, found it annoying, and apparently the Git maintainers did as well. They added what they called opportunistic updates.

If we've just fetched master from origin, and if origin has default remote.origin.fetch refspecs that include refs/heads/master:refs/remotes/origin/master—with or without a force flag, and after expanding * in any refspecs if appropriate—then git fetch, since 1.8.4 anyway, will go ahead and update refs/remotes/origin/master now.

These opportunistic updates work any time they can: git push origin master checks to see if your push succeeded, and if so, updates your origin/master appropriately. (This was in Git long before 1.8.4, which is why not doing it on fetch was so inconsistent.) Similarly, git fetch origin master, which "means" git fetch origin refs/heads/master: and hence does not update a destination ref, still opportunistically updates the remote-tracking name per the default fetch refspec.

This all requires that you use the name origin, so that Git can look up remote.origin.fetch. That's why using the URL that origin stands for causes the lack of opportunistic updating. The URL isn't a remote, and Git doesn't find the remote.origin.fetch settings and cannot apply the opportunistic update rules.


5The force flag means do this update even if it's not a fast-forward, which means you need to search for what fast-forward means in Git. I'll refer here to another answer I wrote about git fetch. This description applies to fetch and push, but git push now has a fancier version, --force-if-lease, that git fetch lacks.

6Unless you set fetch.prune to true or use the various prune options, however, git fetch still leaves stale remote-tracking names behind. I'm going to ignore this problem here.


What this means for you, as a useful special effect

Since git fetch and git push take refspecs, and fetch opportunistically updates anyway, and refspecs follow fast-forwarding rules, you can run:

git fetch origin refs/heads/br1:br1 refs/heads/br2:br2

as long as you're not on either branch right now. Your Git will call up the Git at origin, look up their branch names, and then:

  • update your remote-tracking origin/br1 and origin/br2, with forcing, opportunistically; but also
  • create or update your refs/heads/br1, rejecting (as a non-fast-forward) the update if your br1 is ahead of theirs, and likewise for your branch br2.

(I never actually do this myself, and if you have fetch.prune set to true and do some wildcarding with *, you can shoot yourself in the foot this way. I clobbered a few refs in one of my junk repos I use for this, while writing this up. So avoid wildcards here, especially if you turn on pruning.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks for your detailed explanation torek. Would you summarize somewhere in your answer, what exactly people should do, to force push a Git Subtree please? better explain both the 3-step and 1-step approach. I included my "total disaster" to illustrate/explain the situation that I've tried to comprehend the somewhat-explained command as much as I can possible do, but still screwed myself up, because my lack of understanding what should be doing in my/common situation, instead of that specific one. thx. – xpt Nov 06 '21 at 15:32
  • Updated in OP that, _"I tried to create a minimum set of steps to dup the situation, and I've exhausted everything possible I could think of, but still unable to do the force push to my gist subtree."_ – xpt Nov 06 '21 at 16:23
  • One more thing torek. Seems to me that you've missed the critical point of what I was talking about, as I didn't find a single reference to "subtree" in your answer. I can understand they are all the same thing in your eyes, but please understand it's a total different beast in my eyes. So, please give the simplest way people can do force push a Git Subtree please. – xpt Nov 06 '21 at 16:31
  • Well, I don't use `git subtree` myself (it's not been maintained and is rumored to have various bugs), but if the result of force push is a total disaster, I don't know what you *want*. I only answered the force-push question: your final command achieved the same result that you'd get with `git push --force origin for_force_push:master` would have (except that the 3-command variant keeps a branch name around on your side, rather than just the remote-tracking name `origin/master`—whether that's of any *use* to you, I don't know). – torek Nov 07 '21 at 03:11
  • what I want has been stressed multiple times at the multiple places -- simplest way for people to force push a Git Subtree. That's it. – xpt Nov 07 '21 at 03:55
  • That's why there's a "TL;DR" section at the top: to force push using names, use what I showed. – torek Nov 07 '21 at 04:21
  • up-vote for your detailed explanation, but I'll wait for another answer on ways for people to force push a Git Subtree. thx for helping. – xpt Nov 07 '21 at 15:19