Fabian
Fabian

Reputation: 5650

git checkout to latest commit on current branch

I try to find a way to checkout to the latest commit in the current branch of a repository. I think this is trivial, I just can't come up with the right incarnation of commands.

To describe my problem a bit more in detail. This is what I want to achive. I want to automatically update the working directory of a git repository with the latest version from server but keeping whatever branch the repository is on right now and I don't care about local changes.

So first I run git fetch to get all commits from server. Then I would do git checkout HEAD to advance to the latest commit or even git chekout -f HEAD since I don't care about local changes. Unfortunately this tells me:

Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded. (use "git pull" to update your local branch)

But I do not want a pull since that would merge with local changes imho. A git reset --hard also does not work since it would not advance to the latest commit. And a git chechout master also does not work since I don't know if this repository is on branch master or a different one.

So I think the command I am looking for would be something like this:

git checkout -f `git show_lastest_commit_on_current_branch`

P.s.:

While writing this I think I came up with a solution. First run a git reset --hard to throw away any local changes and then I can safely run git pull since there is nothing more which could create a conflict. Is this the right thing to do?

Upvotes: 1

Views: 4918

Answers (1)

torek
torek

Reputation: 488183

I probably would not do this in the first place. I'm not sure what your setup is, but in general, if there's anything that git reset --hard would obliterate, no automated script should be doing that at all. If you want to have automated deployment, the deployment area probably should not be using Git this way in the first place. (That is, “don't use Git at all” or “use Git, but treat the entire deployed branch as ‘build artifacts’ that are not source-controlled” would be the rule here.)

That said, if I had to do this, I'd probably go for the method ElpieKay outlined, or something equivalent, such as git fetch && git reset --hard @{u}.

Finding the current branch

Note that if you want to get the name of the current branch, there are two ways to do this in a shell (sh or bash) script:

branch=$(git symbolic-ref HEAD) || exit
branch=${branch#refs/heads/}

Now $branch is, e.g., master or deploy or whatever. If the repository is in "detached HEAD" mode, the call to git symbolic-ref HEAD printed:

fatal: ref HEAD is not a symbolic ref

to standard error, and then the || exit clause made the script quit. (Adjust behavior as desired by changing the script; add -q to suppress Git's message to stderr.)

Alternatively:

ref=$(git rev-parse --symbolic --abbrev-ref HEAD)

Now $ref is, e.g., master or deploy or whatever, unless the repository is in "detached HEAD" mode, in which case $ref is the literal string HEAD. (This is why I used the name ref rather than branch.)

Using @{u}

The @{u} syntax is short for @{upstream} and is described in the gitrevisions documentation. In General, any place where Git needs a particular commit ID, you may spell it with any number of revision specifications. A branch name like master simply translates to the tip commit on that branch. Adding @{upstream} directs Git to:

  • find the current branch (much as we did above);
  • look up branch.$branch.remote, e.g., branch.master.remote is probably origin
  • look up branch.$branch.merge, e.g., branch.master.merge is probably refs/heads/master
  • combine the two lookup-results and apply the mapping rules in remote.remote.fetch, e.g., remote.origin.fetch is probably +refs/heads/*:refs/remotes/origin/*
  • use the mapping output as a symbolic name for the appropriate remote-tracking branch, e.g., refs/remotes/origin/master is the full spelling for origin/master

Thus, git reset --hard @{u} "means" git reset --hard origin/master if you're on branch master and your local master is set to track remote-tracking branch origin/master.1 Note that because @{u} looks up the current branch, it fails immediately if you are in "detached HEAD" mode: a detached HEAD is not a named branch2 and hence cannot be tracking anything.


1There are too many occurrences of the words "branch" and "track" in this, but that's how Git spells it out: a local branch (by name, such as master) is allowed to track one other branch. The other branch that it tracks is usually a remote-tracking branch such as origin/master. So:

  1. master is a branch (or more precisely, a branch name);
  2. master-the-name tracks something;
  3. the thing it tracks is another name;
  4. the other name is refs/remotes/origin/master, or origin/master for short; and
  5. origin/master is a remote-tracking branch.

When the local branch named B is tracking a remote-tracking branch RB, the remote in question, and/or that branch on that remote, is what we (and Git) call the upstream.

2In fact, a detached HEAD behaves just like a branch, except that it has no name—or, for some purposes, it has the name HEAD. You cannot set branch.HEAD.remote and branch.HEAD.merge, though.


The difference between git fetch origin and git fetch origin master

Assume we've set $branch as above. Let's also set up $remote from branch.$branch.remote:

remote=$(git config --get branch.$branch.remote) || \
    die "branch $branch has no remote"

(where die does the obvious thing).

We are interested in the difference between these two alternatives:

git fetch $remote $branch && git reset --hard FETCH_HEAD  # alternative 1
git fetch && git reset --hard @{u}                        # alternative 2

If your Git is not older than version 1.8.4, there is only one difference here: adding $remote $branch—let's say this is origin master for discussion purposes—tells your Git to bring over only the updates that go into your origin/master, rather than updating all of your remote-tracking branches.

That is, git fetch origin gets you all the updates from remote origin, while git fetch origin master gets you only the updates to their master. If there are other updates to other branches, you skip them for now. (You may have to pay the cost to get them later, and/or any such costs are typically tiny enough not to matter anyway.)

If your Git version is very old, though, there is one more difference: specifically, Git versions predating 1.8.4 fail to update origin/master, putting the new information only in the special FETCH_HEAD file. (The FETCH_HEAD file is mainly meant for the git pull script to use. It records, for git pull's purposes, everything that git fetch brought over. Remember that git pull simply runs git fetch first, then runs git merge unless you direct it to run git rebase instead. The merge-or-rebase step uses the information saved in FETCH_HEAD to know how to do the merge-or-rebase.)

Which one should you use? That's up to you. Alternative 1 avoids fetching extra branches but requires setting up variables $branch and $remote. This in turn forces you to decide up front what to do if you're not on any branch (if you're in detached HEAD mode). Alternative 2 is easier to write, and simply fails automatically if you're in detached HEAD mode. Alternative 2 updates all your remote-tracking branches (both good and bad, considering the extra work in fetching) and papers over the difference between Git 1.7 (pre-1.8.4) and Git 2.10 (post-1.8.3). Papering over this difference is either neutral or good.

The @{upstream} syntax itself was new in Git version 1.7. As far as I know, even the oldest-and-worst Linux distributions come with Git version 1.7.1 now, so there is probably no need to worry about someone still using 1.5 or 1.6.

Note that reset handles upstream rebases

Any attempt to use git pull, which runs git merge, can cause you trouble if your upstream does a rebase, or strips out commits. This is because your remote-tracking branch (origin/master or whatever) is moving in a way that does not simply add new commits. When that happens, Git will generally consider commits on your branch (your master) to be commits that you made, that they never had. The git merge step will merge "your" commits with their latest, effectively resurrecting commits your upstream thought they had sent down the memory hole.

If your upstream never does this, it is not a problem.

Upvotes: 3

Related Questions