Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Other consequences of `git push --force`?

Questions and answers about forcing a git push aren't hard to find (here are two). The standard answer goes something like this:

If you have to force a git push, technically you can with the --force option, but procedurally you shouldn't because someone may have already pulled and somewhere a kitten will die.

I see this as generally sage advice - the safer route is to just commit again, fixing whatever you broke. But let's say for example I know (magically) that no one has pulled the commit(s) yet. Or better still, that this is a private repo and so there's no danger of breaking anyone else's clone in the first place.

Are there any other [negative] consequences of --forceing a push, specifically technical consequences? Maybe it causes some extra garbage in the remote, or it breaks version 1.2.3 of X Corps' analysis tool, or it makes rebasing more confusing later on, etc.. Anything?

EDIT 1: I have my own anecdotal evidence suggesting that --forceing a push on a private repo doesn't seem to cause any problems. I'm not interested in perception, though; I'm looking for references and/or proof of such.

like image 549
Chris Tonkinson Avatar asked Jan 21 '14 13:01

Chris Tonkinson


3 Answers

A force-push merely tells the remote to move the given label(s) even if the move is not a fast-forward operation.1 A non-fast-forward can (does not necessarily) result in "abandoned" commits: commits that are no longer reachable from some reference.

(It does not do this when the label motion occurs on a part of the graph that is referenced elsewhere. For instance, if branch xyzzy points to commit D in the sequence:

A-B-C-E  <-- plugh
   \ /
    D    <-- xyzzy

then it is irrelevant what happens to label xyzzy, as label plugh makes commit D reachable. So moving xyzzy in a non-fast-forward fashion to, e.g., point to commit C does not affect commit D at all. Likewise, deleting label xyzzy entirely is also harmless, at least in terms of the commit graph structure.)

Remote bare repositories (to which one generally pushes) often do not log all ref updates, so this tends to trigger rapid garbage-collection of any abandoned commits. (Note that if you keep them in your own repository, you can restore gc'ed commits later—but that requires that you send the data over the network again, and exposes you to data loss risk if your own repo gets corrupted via, say, a power failure, or your computer catching on fire.)

If you have some third-party software that assumes that (some or all) branch labels only move in fast-forward fashion, said software could fail in interesting ways. I know of no such software and if it exists I would call it "broken", but people do often seem to write broken code and then depend on it.


1Older versions of git allowed tags to move without a force-push if the move was fast-forward, in the same way that branch labels are expected to move. Newer versions (I think introduced in 1.8.2, but that's just from memory) reject any tag motion unless you use force. Of course you can always delete, then re-create (possibly at a different point), a label, and the built-in hook is OK with that. So there are other ways to move a tag or branch, in arbitrary fashion, even without force. Just make sure the parts of the commit graph you want to retain have some label across each operation.


As for making (your own) rebasing harder later: yes, it can have that effect, because you might force-push from "desktop" and thus, in effect, rearrange "shared-bare-repo". Later, on "laptop", you might not be able to remember just which commits you rebased, and figuring it out can be a little tricky, especially in a large, highly-active repository.

Fortunately, the new upcoming git release has a new feature that uses your reflogs to figure out if and when an "upstream rebase" has occurred. That is, in the above situation, you would be able (on "laptop") to ask git to automatically discover which commits were rebased in shared-bare-repo and "re-rebase" your laptop work on that. (This is a generalization of the method used with git pull --rebase today, in git 1.8.x.)

like image 200
torek Avatar answered Oct 24 '22 03:10

torek


To complement torek's excellent answer:

A force-push can cause problems with later merges.

The problem:

If you force-push a branch A, you are removing some existing commits from that branch (otherwise you would not need to force). If (as described in torek's answer) these commits are also referenced from another branch B, then they will remain in that branch. If you later later merge the two branches A and B, A will contain the "new" (forced) commits, and B "old" commits.

In this situation git merge will probably not do what you want. In the best case, you will get merge conflicts. Worst case, you will not but the result will still be wrong. For example, if you removed a commit c1 from A using git rebase -i, if c1 is also in B it will be re-introduced if you merge A and B. That might silently re-introduce a bug :-(.

So if you force-push, make sure the commits you remove are not referenced from other branches / tags (or force-push these branches/tags as well).


Illustration of a deleted commit "returning"

Run the bash script below. The script creates a git repository, and branches master, branch1 with the following commits:

  • good commit (branch1)
  • bad commit (master)
  • Initial

Then it:

  • uses git reset --hard to discard "bad commit" from master
  • merges branch1 into master

Output from the script:

Initialized empty Git repository in ....
[... creating commits ...]
### Bad commit in history:
* 7549dcc (HEAD, master) bad commit
* 31afec8 Initial
### Removing bad commit:
HEAD is now at 31afec8 Initial
### Bad commit no longer in history:
31afec8 (HEAD, master) Initial
### Merging branch1:
Updating 31afec8..be801e5
Fast-forward
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 b
 create mode 100644 c
### Bad commit is back!
be801e5 (HEAD, master, branch1) good commit
7549dcc bad commit
31afec8 Initial

As you can see, "bad commit" is removed from master using git reset --hard. However, "bad commit" is still contained in branch1, so when branch1 is merged into master, "bad commit" returns.

Note: This example does not actually force-push, but if you worked with an upstream repo, you would have to force-push after removing the bad commit - the result would be the same.

The script:

#!/bin/bash
set -e
if [ -e gitrepo ]; then echo "Please remove gitrepo"; exit 1; fi

git init gitrepo; cd gitrepo; git config core.pager cat
touch a; git add a; git commit -m Initial
touch b; git add b; git commit -m "bad commit"
git checkout -b branch1; touch c; git add c; git commit -m "good commit"
git checkout master

echo "### Bad commit in history: "
git log --oneline --decorate

echo "### Removing bad commit: "
git reset --hard HEAD~1

echo "### Bad commit no longer in history: "
git log --oneline --decorate

echo "### Merging branch1: "
git merge branch1

echo "### Bad commit is back!"
git log --oneline --decorate
like image 28
sleske Avatar answered Oct 24 '22 03:10

sleske


I use --force on my private repositories all the time without adverse side effects, because I know when I've forced a change and what the consequences are to my code.

The only problem with --force is that it changes history in an upstream repository and someone may have made changes to the state of the repository before history was changed. This causes problems - and is why it is not a recommended solution. But even then - these problems can be fixed, they just take a bit of work.

So to answer the question - if you just want to share code with yourself in a private repo and nobody else is going to be affected by these changes, there are no other side effects.

like image 1
Abizern Avatar answered Oct 24 '22 02:10

Abizern