Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Git shortcut for checkout, pull, checkout, merge/rebase

Let's say I've got 2 branches, master and codyDev. Before I begin making changes on codyDev, I do the following in order to make sure I'm starting from the latest master commit on codyDev:

git checkout master
git pull
git checkout codyDev
if changes, git merge master

Is there a shortcut for the first 3 steps, so I don't have to leave my codyDev branch? I tend to find out that the master has not been updated and the checkout/pull/checkout was 3 unnecessary commands.

Note that I did Google to try to find an answer to my question, but there seems to be a wide range of potential and somewhat complicated solutions. The one that seemed most appealing was something to the affect of git fetch origin master:master, but the explanations I read were not super clear because the examples were a bit more complicated. I also found myself wondering "why a fetch instead of a pull" (i.e. git pull origin master:master)?

like image 648
codenaugh Avatar asked Sep 13 '16 00:09

codenaugh


3 Answers

You can update your codyDev branch with the latest changes from master without actually changing branches:

git checkout codyDev      # ignore if already on codyDev
git fetch origin          # updates tracking branches, including origin/master
git merge origin/master   # merge latest master into codyDev

The trick here is that to merge the latest master into codyDev you don't actually need to update your local master branch. Instead, you can just update the remote tracking branch origin/master (via git fetch) and then merge it instead.

You can also directly update the local master branch using git fetch without actually checking out the local master branch:

# from branch codyDev
git fetch origin master:master

However, this will only work if the merge is a fast-forward of the local master branch. If the merge be not a fast-forward, then this will not work, because a working directory is required in case merge conflicts arise.

Read here for more information.

like image 111
Tim Biegeleisen Avatar answered Oct 17 '22 07:10

Tim Biegeleisen


You could add an alias to your ~/.gitconfig.

[alias]
start = !git checkout master && git pull origin master && git checkout -b

Or run this to add it from the command line :

 git config --global alias.start '!git checkout master && git pull origin master && git checkout'

Also, it might not be clear from the code above, but make sure there is a space after -b.

You could also omit the -b and pass it in so you have the option of operating on an existing branch. In that case, you would have :

[alias]
start = !git checkout master && git pull origin master && git checkout 

And then to call it would be

git start -b newBranch

Or

 git start existingBranch
like image 44
swestner Avatar answered Oct 17 '22 09:10

swestner


All "perfect" solutions require something at least a little tricky, or Git's new (since version 2.5) git add-worktree feature. Tim Biegeleisen's method is the simplest method, and maybe the best, but not exactly the same as your current workflow, though perhaps your current workflow is not ideal anyway.

There are several keys here:

  • First, git pull is itself just a shortcut. It "means" (slightly roughly) git fetch && git $something, where $something is either merge or rebase. Which one you get—merge vs rebase—depends on how you have configured your Git repository and branches.

    The git fetch step is where your Git actually picks up new commits from origin, if there are any to pick up.

  • Second, git merge may do something called a fast-forward merge. This is kind of a misnomer, because a fast-forward is not actually a merge at all. And, while git rebase just does a rebase, if you have no commits of your own, the effect is the same as a fast-forward.

  • Third, you need to understand how Git manages your commit graph and branch labels. This is going to tie together the fetch and the not-quite-merge fast-forward.

  • Fourth (and perhaps the most complicated part), git fetch does something interesting using what Git calls refspecs. This is why git fetch origin master:master is interesting, and also answers the question:

    "why a fetch instead of a pull" (i.e. git pull origin master:master)?

    (in fact there is no such thing, because git pull bypasses the fetch refspecs when it runs git fetch for you).

Let's start with the third item first, because it's actually quite fundamental. If you already know all this, feel free to skip forward...

The graph and the labels

Git's commit graph is a Directed Acyclic Graph or DAG. This actually means a lot, but for simple usage purposes we can boil it down to this: each commit has an ID—the big ugly SHA-1 hashes like 7452b4b5786778d5d87f5c90a94fab8936502e20—and each commit records its parent commit ID(s), which, in effect, means each commit "points back" to its parent(s). Most commits have just one parent, but merge commits have two (or more but we'll stick with two). There's also at least one commit—the first commit you (or whoever) made—that has no parent: it can't have had a parent, because it was the first commit.

This means we can draw the commit graph. If we put older commits on the left and newer ones on the right, we get something like this:

o <- o <- o   <-- master

This graph has no merges at all and just three commits: the first one ever, then the second (which points back to the first), and finally the third (which points back to the second). Each commit is kind of boring so we have a little round o, and each has one arrow pointing left, to its parent.

I also threw in the branch name, master. It points to the last commit on the branch—the third commit in the graph. This last commit is the tip of the branch, and a branch name is really just a pointer to the tip-most commit.

What this means is that if we add a new commit, the branch name moves, so that it still points to the tip-most (newest) commit. This new commit points back to where the branch-name used to point:

o <- o <- o <- o   <-- master

All the inner arrows are kind of boring as well (we know they point leftward, to older commits) so I like to draw these graphs in a more compact way:

o--o--o--o   <-- master

This leaves a lot more room for more complicated graphs that branch:

o--o--o     <-- master
    \
     o--o   <-- dev

and then maybe merge again (here dev is "merged into" master, i.e., someone did git checkout master && git merge dev):

o--o--o---o   <-- master
    \    /
     o--o     <-- dev

They might also keep going:

o--o--o---o    <-- master
    \    /
     o--o--o   <-- dev

(After someone merged dev into master, someone—maybe the same someone, maybe someone else—made another commit on dev.)

The first tricky part

We already mentioned that the branch name, like master or dev, points to the tip-most commit on that branch. But earlier commits are also on the branch, and once we have a merge commit—a commit with at least two parents: two left-pointing arrows—this causes a bunch more commits to suddenly be on the branch. That is, now a bunch of commits are on both branches.

Let's look at this graph again, but use single uppercase letters for each commit, instead of just an o:

A--B--D---F    <-- master
    \    /
     C--E--G   <-- dev

Commit F is the merge commit. Note how it has two leftward links, one to D and one to E. (The arrow to E points down-and-left, and the arrow from C to B points up-and-left, but they still point left, i.e., to some earlier commit.) Commits C and E are on branch dev, but they are also on master. Commit A is on master, but it is also on dev.

In Git, a commit can be on more than one branch.

The more general term here is "reachable": we look at commits in terms of how (and whether) we can reach them while following the one-way arrows that go from newer commits to their older parent commits. When we reach a merge, which has two parents (or more, but again we'll stick with two), we take both paths simultaneously.

The second tricky part

A branch name simply points to the tip commit of the branch. But more than one name can point to some commit. In the graph above, master points to F and dev points to G. If we check out branch master, then create a new branch feature, this new branch will also point to F:

A--B--D---F    <-- master, feature
    \    /
     C--E--G   <-- dev

If we now make a new commit H, branch feature needs to change to point to H, while H needs to point back to F. That is, we need to get this new picture (or some variation on it—there are always many ways to draw each graph):

            H   <-- feature
           /
A--B--D---F     <-- master
    \    /
     C--E--G    <-- dev

How does Git know to move feature and not master?

Both of them point to commit F. If all Git knew is that F was the current commit, Git wouldn't know which branch name to change. Clearly Git must know which branch we're actually on. And it does: that's what the file HEAD is for. The HEAD file contains the name of the current branch.

HEAD contains the name of the branch, and the branch name contains the tip commit ID. This is what it means to be "on a branch" in the first place: if you're on branch master, HEAD has the name master in it. If you're on dev, HEAD has the name dev. If you're on feature, HEAD has feature in it. Whatever branch you're on, that's what's in HEAD.1

The git checkout command changes the branch name stored in HEAD. (It does more as well, but that's how it changes which branch you are on.) The git commit command, once it writes a new commit, stores the new commit's ID in the current branch, read from HEAD. And, the new commit's parent ID is usually whatever commit HEAD points to, or rather, whatever commit the branch-that-HEAD-points-to, points to.2 That is, HEAD points (indirectly) to the current, branch-tip, commit, and the new commit then points to that as its parent and git commit updates the branch name and now the new commit is the new branch-tip.


1A "detached HEAD", which sounds scary, just means that HEAD has the raw hash ID of a commit in it instead of a branch name.

2I say "usually" because git commit --amend works by setting the new commit's parent to the current commit's parent, instead of to the current commit itself. In effect, instead of adding a new commit to the chain, it shoulders the current commit aside. The new commit is still the end of the chain, but now the new current commit's parent linkage bypasses the "amended" commit, making it seem to be gone.


Merging

Merging itself can be complicated and messy, but the act of making a merge commit is extremely straightforward. Git makes a new merge commit in exactly the same way as it makes a regular (non-merge) commit, with the exception that the new commit has two (or more) parent IDs stored in it.

The first parent ID is the same parent as always: the current branch's tip-most commit, i.e., the HEAD commit.

The second parent ID is the commit you just merged: the tip of the other branch.

This is how Git made commit F in our example graph. We ran git checkout master first, so that HEAD said master and master said D. Then we ran git merge dev, and dev said E:

A--B--D     <-- master
    \
     C--E   <-- dev

Git did the stuff it needs to do, to come up with the files to stick into the new merge commit. This "stuff" consists first of finding the merge base: the commit where the two branches join up. That's commit B. Git figured out how to merge the changes on master, from B to D, with the changes on dev, from B to E. Then Git made the new commit F. F's first parent is D and F's second parent is E, and once F was all made, Git wrote its ID into master:

A--B--D---F   <-- master
    \    /
     C--E     <-- dev

That's how real merges work; but what about fast-forwards?

Git had to make a real merge here because of commit D. Let's make a new, different repository and make some commits:

A--B        <-- master
    \
     C--D   <-- dev

Now suppose we git checkout master here, and run git merge dev. The tip of dev is D and the tip of master is B. The merge base is where the branches first join up, which is commit B again.

Notice anything special about B?

It's the tip of master. The current commit, B, is also the merge base. There are no changes from B to B: B is already itself. There are changes from B to D, on dev, but master has no changes of its own.

What this means is that Git can "slide the branch label forward". Instead of having master point to B, Git can just slide the label forward to D. (This goes against the direction of the arrows, i.e., forward in time instead of backward, but it is easy for Git to find, since we told Git to look at dev, which points to D. Git just has to notice that the current commit, B, is already an ancestor of D.)

So now Git does a fast-forward label move:

A--B
    \
     C--D   <-- dev, master

and now master points to commit D. The graph no longer even needs the kink in it—we only had to draw it this way to leave room for master to point to commit B. Now we have a simple A<-B<-C<-D chain with both labels pointing to D.

This is what a fast-forward is: the act of "sliding a branch label forward, against the direction of the parent arrows." Note that it cannot be done if there's a commit in the way:

o--*--o      <-- br1
    \
     o--o    <-- br2

You can't "slide br1 forward" to point to the tip of br2. You would first have to slide it back to the merge base * commit before you can slide it forward. If you move br1 to point to the same commit as br2, the final commit along the top line is no longer reachable from br1, and it stops being on branch br1. If there is no other label pointing to that tip commit (or to a commit that points to it), it gets "lost".

git fetch and git push

The fetch and push operations both do the same kinds of things, so we can cover both of them here.

In both cases, you have your Git call up another Git, usually over ssh:// or https://, i.e., over the Internet-phone. Your Git and their Git then have a little talk, where yours and theirs find out which commits you have that they don't, or vice versa. (Which one depends on whether you're fetching or pushing.)

Then, after the two Gits have agreed about which commits they both know about—using their SHA-1 hash IDs, which are always the same on both sides3—the "sending" Git sends over the commits and other objects needed, and the "receiving" Git saves those away in its repository. Then, at the end of all of this, whichever end is sending commits has also sent some <branch-name, tip-commit> pairs. When you are git fetch-ing from origin, origin's Git sends your Git these name/ID pairs. (When you are pushing, your Git sends the pairs, and this is where the symmetry between fetch and push is deliberately broken.)

Let's look exclusively at the git fetch case now. In normal, standard setups,4 their Git sends you all their branch tips and all the commits and other objects your Git needs to use those. Your Git then renames the branch tips: instead of "branch master", your Git calls it "remote-tracking branch origin/master". (Here I'm assuming the remote is named origin.) Instead of dev, your Git uses origin/dev. In fact, there's a slightly longer, more precise form for this: it's actually +refs/heads/*:refs/remotes/origin/*. But the key take-away here is that there is this renaming that occurs, from branch to origin/branch, so that their branches become your remote-tracking branches.

In any case, when your Git updates your remote-tracking branches, it does a "forced update" if needed. That is, when it's updating your origin/master, your Git first checks whether you have origin/master at all. If not, it just creates it, pointing to the same tip commit we just got from them. If so, your Git checks: is this a fast-forward? If it's a fast-forward operation, your Git updates your origin/master using a fast-forward. If not, your Git says: well, we'll just forget some commit(s) we used to have on origin/master, and take their master and call it origin/master anyway. We'll just re-set our origin/master as a "forced update":

Receiving objects: 100% (992/992), 370.76 KiB | 0 bytes/s, done.
Resolving deltas: 100% (775/775), completed with 142 local objects.
From [url]
   e0c1cea..9194226  maint      -> origin/maint
   6ebdac1..35f6318  master     -> origin/master
   f2ff484..15890ea  next       -> origin/next
 + 7d82ce0...eb0e753 pu         -> origin/pu  (forced update)
   fa7f92e..2d15542  todo       -> origin/todo

That's what the "forced update" means: that the update was not, in fact, a fast-forward operation.


3This almost magical trick is the key to distributing repositories. Both Git and Mercurial use this kind of universal hash ID to make it all go. The details are beyond the scope of this article.

4"Mirror" repositories do this differently, but all through refspecs.


refspecs

These updates are controlled by refspecs. A refspec is, in its second-simplest form,5 just a pair of branch-names with a colon between them: master:master, or master:origin/master. The name on the left is the source, and the name on the right is the destination.

For fetch, the source is the remote Git, and the destination is your own repository. (For push, this gets reversed, of course.)

A refspec can start with a leading plus sign, as in +master:origin/master. This leading plus sign is the "force flag". More precisely, it's a per-refspec force flag: you can set it, or leave it out, of each refspec (vs the global --force option, which sets it for every refspec on the command line).

If you leave out the force flag, Git will only do fast-forwards.

This is the key to using git fetch to update your own branches. A fast-forward update does not lose commits. A forced update could lose commits. So if you run git fetch with a pair of branch names, like master:master, your Git will only update your master to match the remote's master if that update is a fast-forward.

There's a serious complication here though. What if it's not a fast-forward? What if the upstream Git users have "rewound" the branch, the way the Git repository for Git rewinds the pu (pickup) branch all the time? (Note the "forced update" above.) If your Git fetches a bunch of commits, but then sees that the update is not a fast forward, your Git simply doesn't update your labels, unless you've set the force flag.

This is why Git uses remote-tracking branches. Since the remote-tracking branch origin/master is not just master, it's safe to update origin/master, even as a forced update. Either you have your own branches that hang on to commits that the upstream is trying to retract, or you don't. If you do have those commits on branches of your own, your Git still has those commits, because your branches are unaffected. If you don't, Git assumes you don't care about the retraction: your remote-tracking branches are going to track the remote.

(If you run git fetch twice, with a few minutes in between, and you get some update and then it gets retracted ... well, if you hadn't run that first git fetch just then, you never would have seen the retracted commits. This means they can't be that important. If you saw them and wanted to hang on to them, you would have made a branch.)

There's one more complication here though: Git normally won't let git fetch—or git push, for that matter—update the current branch (the one whose name is stored in HEAD).6 The reason for that has to do with something we skipped over, above: when you git checkout a branch, Git does not just write to HEAD. It also fills your index and work-tree from the tip commit of that branch. If git fetch were to change the tip commit, with no notice, your index and work-tree would get out of sync. Worse, if you have some changes in progress, your index would fail to catch up with the new commits.


5The simplest form is without the colon, e.g., git push master. What this means is more complicated, though, because it means different things in git fetch and git push. For git fetch, it means "nothing to update locally", except that since Git version 1.8.4, Git still does "opportunistic updates" of remote-tracking branches. For git push, this simplest-refspec form means "use the upstream name", which is usually the same name as the local branch.

6That is, if there is a work-tree. If the repository is "bare" (has no work-tree), git push will let the current branch get updated.


Conclusion

I have run out of energy :-) but let's see if we can summarize:

  • git checkout master: switches the index and work-tree to master and writes master into HEAD
  • git pull: fetches from the current branch's (master's) remote and then runs git merge:
    • The fetch step brings over their master, which becomes your origin/master.7
    • The merge step either really-merges, or fast-forwards, your master to bring in commits you now have on your origin/master.
  • git checkout codyDev: switches the index and work-tree to codyDev and writes codyDev into HEAD
  • git merge master: does a real or fast-forward merge of your master, which (due to the earlier update) is now merged with, or fast-forwarded-to, the master on the remote (presumably origin).

Instead of doing all this, you can, as in Tim Biegeleisen's answer, just run git fetch or git fetch origin—which brings over all their branches and updates your origin/master remote-tracking branch—and then git merge origin/master.

What's different is now obvious: this does not update your master. Do you need it to? There is one other difference, not quite as obvious: the git merge command will make a default commit message that says "merge remote-tracking branch origin/master" instead of one that says "merge branch master". Do you care? (You can edit the message to remove the remote-tracking adjective and the origin/ part, if you do care.)


7Assuming your Git is 1.8.4 or newer. Otherwise the opportunistic update gets skipped. The pull script still manages to merge the right commit, but your origin/master drifts further and further behind. (If you have an ancient Git, upgrade to a modern Git.)

like image 26
torek Avatar answered Oct 17 '22 08:10

torek