Last Updated: September 27, 2021
·
24.1K
· jwebcat

Let's all take a moment of silence... git pull is dead.

Well... git pull has been on my mind for a while.

This is great however :

Rebasing deletes merge commits!

So this is explained in great detail here the example below is a summary from envato notes

  • Below is a shortened version of the scenario in which git rebase origin/master or git pull --rebase could BITE YOU HARD

Ok, here is a situation where using git rebase origin/master or git pull -rebase could delete your merge commit and annoy you.

It could also be quite BAD if you don't notice it, because when someone looks at your history later it will look like that branch was never merged.

  • you have been doing the below just fine for a while
$ [master] git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: little fix
Applying: forgot to push this

then this

$ [master] git push
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 526 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
To /path/to/your/repo-origin
   f9c3cb8..e4a2e92  master -> master
  • And all is well.

  • Suddenly one day you have been working on a feature branch for a while and you merge to master using --no-ff of course.

[master] git merge --no-ff feature
Merge made by recursive.
 b |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)
 create mode 100644 b
  • but, when you go to push, someone has already pushed before you. So, git pukes and says
[master] git push
To /path/to/your/repo-origin
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '/path/to/your/repo-origin'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes (e.g. 'git pull') before pushing again.  See the
'Note about fast-forwards' section of 'git push --help' for details.
  • so without hesitation we of course do
[master] git fetch
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /path/to/your/repo-origin
   49ab1cf..9f3e34d  master     -> origin/master
  • followed by
[master] git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: my work
Applying: my work
Applying: my work

now our merge commit from our feature branch we were trying to push to master has disappeared! Why?! WTF?! well...

  • This is how git rebase works.

  • so we do the following to rescue our merge commit

[master] git reset --hard origin/master
HEAD is now at 9f3e34d sneaky extra commit
[master] git merge --no-ff feature
Merge made by recursive.
 b |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)
 create mode 100644 b

So what to do? Luckily, git rebase -p comes to save the day.

-from the git man page

-p
--preserve-merges
Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it with the --interactive option explicitly
is generally not a good idea unless you know what you are doing (see BUGS below).
  • basically don't use --interactive flag with the -p flag or weird things will start to happen.

So instead of doing a git pull --rebase you do the following instead

[master] git fetch
remote: Counting objects: 5, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /path/to/your/repo-origin
   49ab1cf..9f3e34d  master     -> origin/master

and then

[master] git rebase -p origin/master
Successfully rebased and updated refs/heads/master.
  • and Voila' your up to snuff and your precious merge commit from feature branch is preserved!
  1. Caveats of using this method.
  2. ORIG_HEAD is no longer preserved git rebase -p sets ORIG_HEAD for each commit being rebased, so you can’t use it to quickly return to the start of a rebase.
  3. Branch tracking is not used Unlike git pull --rebase, which will fetch changes from the branch your current branch is tracking, git rebase -p doesn’t have a sensible default to work from. You have to give it a branch to rebase onto.

Aliases for bash, fish, and zsh to make this painless

here is the OG gist

You can download this gist from your shell using wget or curl like this

-for curl

$ curl -LJO https://gist.github.com/geelen/590895/download

-or if you just want a certain file out of the gist use it like below

$ curl -LJO https://gist.github.com/geelen/590895/raw/ad
75e7bfd9eb08704e90413cf6cad9a4f07f2a71/bash_or_zsh.rc
  • Now for wget
$ wget --no-check-certificate --content-disposition https://gist.github.com/geelen/590895/download

or for a single file from the gist

$ wget --no-check-certificate --content-disposition https://gist.github.com/geelen/590895/raw/ad75e7bfd9eb08704e90413cf6cad9a4f07f2a71/bash_or_zsh.rc
  • here are the aliases for for bash and zsh if anyone is just in the copy paste mood. Place these in your .bashrc for a non-login shell or for a login shell your .bashprofile

  • Edit git_current_branch function below now more efficient thanks to @jussi-kalliokoski ;)

function git_current_branch() { git symbolic-ref --short HEAD 2> /dev/null; }

alias gpthis='git push origin HEAD:$(git_current_branch)'
alias grb='git rebase -p'
alias gup='git fetch origin && grb origin/$(git_current_branch)'
alias gm='git merge --no-ff'
  • gup will do a fetch of origin and rebase -p of the branch on origin with the same name as the current branch.

  • So now you can just type

$ gup
  • and enjoy.

  • I will be writing a series of protips on git. Keep on the lookout.

Credit and thanks for the git revelations and aliases to @glenmaddern

11 Responses
Add your response

Nice work. Love that aliases. And love that explanation of the risks.

over 1 year ago ·

@emgiezet I appreciate that thanks bro. I will be writing a series of protips on git. Keep on the lookout.

over 1 year ago ·

Nice!

BTW, I think you would achieve the same results with git symbolic-ref --short HEAD

over 1 year ago ·

@jussi-kalliokoski Thanks Bro :) glad you liked it.
In which way would git symbolic-ref --short HEAD achieve the same thing? The same as a git rebase -p? I'm interested to hear more.

How so?

If you in put git symbolic-ref --short HEAD into your shell it will just echo out to the current branch you are on.

example:

(cat) $ git symbolic-ref --short HEAD
cat
  • you can see git just tells us that we are on a branch called cat.

  • if we do the same in master

(master)$ git symbolic-ref --short HEAD
master
  • and git tells us we are on master.

How are you using symbolic-ref to recover from an accidentally squashed merge commit?

I thought that if you create a new branch using symbolic-ref it starts a new git history for the new branch that no longer has any connection to the parent repository.

here is a tidbit from git community book

Creating New Empty Branches

Ocasionally, you may want to keep branches in your repository that do not share an ancestor with your normal code. Some examples of this might be generated documentation or something along those lines. If you want to create a new branch head that does not use your current codebase as a parent, you can create an empty branch like this:

git symbolic-ref HEAD refs/heads/newbranch 
rm .git/index 
git clean -fdx 
<do work> 
git add your files 
git commit -m 'Initial commit'

you can also achieve the same effect as using symbolic-ref to create empty branches, in git 1.7.2 + by using git checkout --orphan.

This also creates a root branch, or a branch without previous history.

I would like to know your thoughts on all of the above.

apologies if this bored you to sleep. I wrote this comment with users in mind that might read this thread that are just starting with git.

  • So if you use git symbolic-ref HEAD newbranch it will create a branch called newbranch which is an orphan root branch that no longer shares the same history as the parent repository.

Your thoughts?

over 1 year ago ·

@jwebcat: Oh heh, sorry, I didn't mean you could replace the whole thing with it, I meant you had this:

function git_current_branch() {
  git symbolic-ref HEAD 2> /dev/null | sed -e 's/refs\/heads\///'
}

Which could be simplified to use less processes:

function git_current_branch() {
  git symbolic-ref --short HEAD 2> /dev/null
}

;)

Also thanks for the tip that that git symbolic-ref can be used for creating an orphan branch, I've always used git checkout --orphan for that.

Speaking of orphans, you might find this gist interesting, it's something I made when I had to extract a single file from a repository to its own repository and I didn't want to include the history for other files. Perhaps you even have a better way to do it, I'm extremely interested in learning new things about git as well!

I also use git symbolic-ref for changing the HEAD of remote --bare repos (for those that don't know, the branch set as HEAD is used as default when you clone a repo), since you obviously can't git checkout:

$ git symbolic-ref HEAD refs/heads/my-branch
over 1 year ago ·

@jussi-kalliokoski I added your insights and referenced you bro thanks ;) see the edit to gitcurrentbranch above.

Currently I am hacking on your gist to see what comes of it. Let's see if we can refactor it at all.

I will reply to the gist in a bit.

over 1 year ago ·

great piece of information, that came in the perfect timing.
Thanks!

over 1 year ago ·

The problem isn't pull, it's rebase. Just don't use rebase (or git pull --rebase) and you'll be fine. Rebasing changes history, and the point of a VCS is to maintain history. If you use rebase, you'll need version control for your VCS—that is, Git won't maintain an unaltered record of what came before, which means it's no longer a proper VCS. Rebase looks like it works, but is an accident waiting to happen. Banish it from your Git vocabulary.

over 1 year ago ·

Thanks a lot for the post - very interesting! One comment though - it seems that -p does preserve merge history, but it modifies the recreated commit from which the merge was performed. Below is an example (a bit long) flow that illustrates this issue (output of most commands was removed, only the relevant output was included) - the preserved commit contains the merge result, and not the initial commit as on the feature branch:


git init --bare /tmp/rebase
git clone /tmp/rebase /tmp/rebaseclone1
git clone /tmp/rebase /tmp/rebase
clone2

cd /tmp/rebase_clone1
touch file.txt
git add .
git commit -m 'added file.txt'
git push origin master

cd /tmp/rebase_clone2
git pull
git checkout -b feature
echo 2 file.txt
git commit -a -m 'wrote 2'
git checkout master
git merge --no-ff feature

git logk # logk is an alias for nicely formatted log command.
* f55163e - (HEAD, master) Merge branch 'feature' (43 seconds ago) <Alex Pulver>
|\
| * 2b51d05 - (feature) wrote 2 (43 seconds ago) <Alex Pulver>
|/
* 34edfbc - (origin/master) added file.txt (43 seconds ago) <Alex Pulver>

cd /tmp/rebase_clone1
echo 1 file.txt
git commit -a -m 'wrote 1'
git push origin master

cd /tmp/rebase_clone2
git push origin master
To /tmp/rebase
! [rejected] master -master (fetch first)
error: failed to push some refs to '/tmp/rebase'
...

git fetch
git logk --all
* 7abff92 - (origin/master) wrote 1 (2 minutes ago) <Alex Pulver>
| * f55163e - (HEAD, master) Merge branch 'feature' (5 minutes ago) <Alex Pulver>
| |\
|/ /
| * 2b51d05 - (feature) wrote 2 (5 minutes ago) <Alex Pulver>
|/
* 34edfbc - added file.txt (5 minutes ago) <Alex Pulver>

git rebase -p origin/master
error: could not apply 2b51d05... wrote 2
...
pico file.txt # resolve the conflicts.
git add file.txt
git rebase --continue

git logk --all
* a7e3457 - (HEAD, master) Merge branch 'feature' (86 seconds ago) <Alex Pulver>
|\
| * 4528467 - wrote 2 (2 minutes ago) <Alex Pulver>
|/
* 7abff92 - (origin/master) wrote 1 (5 minutes ago) <Alex Pulver>
| * 2b51d05 - (feature) wrote 2 (8 minutes ago) <Alex Pulver>
|/
* 34edfbc - added file.txt (8 minutes ago) <Alex Pulver>

git checkout 4528467
cat file.txt
1
2

git checkout 2b51d05
cat file.txt
2
</pre>

over 1 year ago ·

@marnen you're right and you're wrong. There's nothing wrong with rebasing a topic branch that only you've worked on to squash it down to change sets that actually make sense and/or to rebase it on top of master before sharing it with others. However, if you really don't want to have a pull erase a merge you've made, then you're right, you shouldn't be doing git pull --rebase. Keep master clean until you're ready to push. This means don't run git merge --no-ff mybranch until you've just run git pull on master. If you didn't do that, then fine, but still don't run git pull --rebase, just do a normal git pull and you'll have an extra merge commit, but that's kind of what you asked for.

over 1 year ago ·

@onlynone The latter approach is mostly the way I use Git. Although I do very occasionally rebase topic branches, I'd normally much rather have an extra merge commit than a false history.

over 1 year ago ·