Let's all take a moment of silence... git pull is dead.
Well... git pull has been on my mind for a while.
-
@cassianoleal wrote a protip about using
git fetch
andgit rebase origin/master
.
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
orgit 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!
- Caveats of using this method.
-
ORIG_HEAD
is no longer preservedgit rebase -p
setsORIG_HEAD
for each commit being rebased, so you can’t use it to quickly return to the start of arebase
. - 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
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
Written by Jupiter St John
Related protips
11 Responses
Nice work. Love that aliases. And love that explanation of the risks.
@emgiezet I appreciate that thanks bro. I will be writing a series of protips on git. Keep on the lookout.
Nice!
BTW, I think you would achieve the same results with git symbolic-ref --short HEAD
@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 am interested how you are using
symbolic-ref
? I am always eager to know more about git.
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 callednewbranch
which is an orphan root branch that no longer shares the same history as the parent repository.
Your thoughts?
@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
@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.
great piece of information, that came in the perfect timing.
Thanks!
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.
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/rebaseclone2cd /tmp/rebase_clone1
touch file.txt
git add .
git commit -m 'added file.txt'
git push origin mastercd /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 featuregit 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 mastercd /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 --continuegit 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
2git checkout 2b51d05
cat file.txt
2
</pre>
@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.
@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.