How can bare Git repository be ahead of my working repository?

306 views Asked by At

I made a bare repository clone of my Git working repository. The remote bare repo is located on network drive and I have been pushing to it so I have backup on different physical media. I am now getting an error on an attempt to push to the bare repository because my local repository is behind the remote repository. All modifications to the remote repository have been by pushing from my working repository. How could my local repo be behind the remote when there have been no other changes made to the remote repo? Here is the error I am getting when trying to push to remote repo

git -c diff.mnemonicprefix=false -c core.quotepath=false push -v --tags --set-upstream AlcatrazVehicleBW1 BitworksDevelopment:BitworksDevelopment FixSetOsDateTime:FixSetOsDateTime HeapUseBy3rdPartyLibBugFix:HeapUseBy3rdPartyLibBugFix M_Bitworks20161226CLOSED:M_Bitworks20161226CLOSED Orig_Port_to_Vx7_Review_changes_by_jdn:Orig_Port_to_Vx7_Review_changes_by_jdn ReSquashM_BitworksIntoMaster:ReSquashM_BitworksIntoMaster master:master
Pushing to //bw1/Public/Bitworks/Projects/Alcatraz/Vehicle.git
To //bw1/Public/Bitworks/Projects/Alcatraz/Vehicle.git
 = [up to date]      FixSetOsDateTime -> FixSetOsDateTime
 = [up to date]      HeapUseBy3rdPartyLibBugFix -> HeapUseBy3rdPartyLibBugFix
 = [up to date]      Orig_Port_to_Vx7_Review_changes_by_jdn -> Orig_Port_to_Vx7_Review_changes_by_jdn
 = [up to date]      master -> master
 * [new branch]      M_Bitworks20161226CLOSED -> M_Bitworks20161226CLOSED
 * [new branch]      ReSquashM_BitworksIntoMaster -> ReSquashM_BitworksIntoMaster
 ! [rejected]        BitworksDevelopment -> BitworksDevelopment (non-fast-forward)
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/FixSetOsDateTime'
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/HeapUseBy3rdPartyLibBugFix'
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/Orig_Port_to_Vx7_Review_changes_by_jdn'
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/master'
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/M_Bitworks20161226CLOSED'
updating local tracking ref 'refs/remotes/AlcatrazVehicleBW1/ReSquashM_BitworksIntoMaster'
error: failed to push some refs to '//bw1/Public/Bitworks/Projects/Alcatraz/Vehicle.git'
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The branch it is rejecting BitworksDevelopment was moved in the local repository to point to the head of the master branch in an attempt to discard the old BitworksDevelopment branch which was squash merged into master. The intent was to start new development branching off of the current master head. My understanding was that the push would overwrite the tracking branch pointer on the remote AlcatrazVehicleBW1 bare repository. It appears there is yet again a Git concept I don't understand.

1

There are 1 answers

7
torek On BEST ANSWER

The branch it is rejecting BitworksDevelopment was moved in the local repository to point to the head of the master branch in an attempt to discard the old BitworksDevelopment branch which was squash merged into master.

Well, that's the problem, then. Note that the easiest thing here would be simply to delete this branch name.

... because my local repository is behind the remote repository

That's the complaint from Git, but the complaint is ... well, "wrong" is the wrong word; maybe a better word is "presumptive".

Remember that in Git, branch names merely point to one particular commit. That commit is the "tip of the branch":

...--A--B         <-- br1
         \
          C--D--E   <-- br2
                 \
                  F   <-- br3

Here, commits A and B are on all three branches, with branch name br1 pointing to B, so that B is the tip commit on br1. Commits C through E are on two branches, with E being the tip of br2, and F is only on one branch, and is the tip of br3.

Git allows you to add new branch name labels, to jump existing labels around arbitrarily, and to delete branch name labels entirely. However, there's a "normal pattern" for name movement, and that's in a "forward-only" direction. Even without adding any new commits, I can move br1 forward, to any of C through F, and Git will believe that this is normal and just do it. I can move br2 forward to F. I can't move br3 forward as there are no commits beyond F.

Suppose, though, I add a new commit G:

...--A--B----------G   <-- br1
         \
          C--D--E   <-- br2
                 \
                  F   <-- br3

Since G's parent is B, I can move br1 forward to point to G. G points back to B, just as C points back to B, so moving br1 to either C or G is OK.

Once I do this move, though, Git resists having br1 move back in any way.

Suppose that I copy G to a new commit G' that I tack onto the end of F:

...--A--B----------G   <-- br1
         \
          C--D--E   <-- br2
                 \
                  F   <-- br3
                   \
                    G'

If I now ask Git to move br1 to point to G', what happens to G?

With nothing pointing to G, we'll "lose" it. Git won't be able to find it, and if I did not save its hash somewhere (in my reflogs, or on my screen), so will I.1 Hence, a request to move br1 from G to G' is a non-fast-forward.2

The basic assumption here is that when you are pushing to a bare repository, if your requested branch label change is not a "fast forward", you probably weren't aware that commit G even exists on the bare repository, as it was probably sent there by someone else, working in parallel, who merely beat you to the git push step. In this case, you need to git fetch from the bare repository to pick up commit G, then figure out how to get your own work to append to the existing commits, rather than removing G. (That would usually be by rebasing or merging.)

To tell a Git server that it should move a branch, even though the motion will lose some commits, you must apply one of the various "force" flags. The easy one to apply is --force or -f, which just tells the server: "I know what I'm doing, lose some commits already." The slightly longer one, which is safer, is --force-with-lease: you have your Git tell their Git "I believe the branch points to commit G, and if so, make it point to G' instead."

The basic assumption is wrong, and G is your commit. You do know it's there, and you are intending to remove it. So, since your Git knows about it, you can use --force-with-lease to check that their (the bare repo's) BitworksDevelopment still points to the same commit, and if so, override it with your new replacement. Or, if you know you're the only one doing things to the bare repository, you can just use plain old --force.

Of course, as in footnote 2, it's actually simpler to just delete the name entirely, as is the more typical pattern with squash-merge. In this case, since the graph looks more like this:

...--A--B--C   <-- master
         \
          ... [the old BitworksDevelopment]

the commits you will be losing by just deleting the branch name are the ones you've squash-merged in as commit C. The name master suffices to hold commit C; you don't need two names for it. At least, not yet: you will want another name for C as soon as you go to make new commits that will come after C, but that you don't want to find via the name master.

Note that Git's protectiveness in bare repositories applies only to non-fast-forward branch name updates. Deleting a branch, losing the commits, is perfectly fine! You do have to use git push --delete origin BitworksDevelopment to send the delete-request to the server, though, so it's not like this will happen by accident. This is quite different from normal development pushes, where the race between two developers, both pushing to the same branch, resulting in non-fast-forwards, is a normal, everyday accident, that deserves protecting-against.


1Bare repositories generally don't have reflogs, and do run a garbage-collect right after pushes, so G might get really-removed pretty quickly.

2The technical definition of a fast-forward branch-name move is that the commit to which the name currently points is an ancestor of the commit to which you propose to point it. Since regular merge commits point back to two or more commits, moving a branch name to a new merge commit that simply combines that branch with another branch, is a fast-forward. The same is true for most squash "merges" as well: we add onto the main branch a single commit that represents all the work done in several commits on the other branch (and then we discard that other branch entirely). The difference in your case is that you're deliberately doing this without discarding the other branch; instead, you're trying to alter the other branch, in a non-fast-forward manner.