1

Is there any way to fetch & check out supplied argument, without caring if it's a branch name or a commit hash?

git fetch
git checkout origin/<branch> or origin/<commit>

how? git checkout origin/<commit> says there's no such branch. A simple git checkout <argument> is impossible because it would not check out remote branch the way git checkout origin/<argument> would.

Rihad
  • 99
  • 1
  • 2
  • 7
  • 1
    Well you could just always default to checking out commits. After all, `origin/some_branch` has a head with a commit too. – Tim Biegeleisen Dec 15 '18 at 08:21
  • This is part of a larger project, user can supply either a branch name or a commit id as an argument. – Rihad Dec 15 '18 at 08:37
  • Why is getting a branch in your working tree so different from getting a commit hash? `git fetch && git checkout ` is enough, but for a branch you have to merge it too: `git fetch && git checkout && git merge origin/`. – Rihad Dec 20 '18 at 10:05

3 Answers3

2

You can to first fetch all remote branches (automatically including the one that has your desired commit) to your local:

git fetch -a

and then simply checkout the commit by its hash:

git checkout <commit-hash>

The fetch is needed first because otherwise your local may or may not be aware of the commit-hash on the remote. Without the fetch, if you were to directly execute the checkout it would complain that the commit-hash is invalid.

bappak
  • 865
  • 8
  • 23
  • Alas, I can't know for sure if the argument supplied is a commit hash or a branch name. – Rihad Dec 15 '18 at 09:59
  • `git fetch -a` means *fetch from all remotes*. You only need `-a` if you have more than one remote name and want to `git fetch origin && git fetch upstream`, for instance. – torek Dec 15 '18 at 10:46
1

The premise of your question is wrong:

A simple git checkout <argument> is impossible because it would not check out remote branch the way git checkout origin/<argument> would.

It's important to realize several interlocking things here about Git:

  1. There is always—well, almost always—a current commit, which you can use the word HEAD to find.
  2. There is not always a current branch, but if there is, it's a branch name, i.e., a reference whose full name has the form refs/heads/name. The same word—HEAD, in all capital letters, finds that branch name. If there isn't a current branch name, Git calls this a detached HEAD.
  3. A remote-tracking name, such as origin/master, is not a branch name. Its full form starts with refs/remotes/ rather than refs/heads/.
  4. If you tell git checkout to check out a commit, but identify it by something other than a branch name, Git will—if the checkout succeeds, that is—produce the detached HEAD state described in point 2. (You can also produce this same state with a branch name, using git checkout --detach.)

The consequence of point 4 above is that git checkout origin/name results in a detached HEAD, the same way that git checkout hash-ID would.

This means your script can just use git checkout <argument>, as it will do the same thing—produce a detached HEAD—if the argument is a hash ID or if it is a remote-tracking name like origin/develop.

Note, however, that if we change this statement to read:

A simple git checkout <argument> is unsuitable because it would not first create, then check out, a local branch based on an existing remote-tracking name, the way git checkout <argument minus the leading origin/ part> would.

we get a true statement: git checkout develop will create a new (local) branch named develop using the name origin/develop (provided, of course, that local develop does not exist yet). However, there's no obvious issue with just allowing <argument> here and having the user provide develop as the name:

#! /bin/sh
git fetch && git checkout "$@"

for instance.

Side notes

There is an interesting consequence of points 1 and 2 here, which is that asking what's the value of HEAD at the moment is really asking one of two different questions:

  • Is HEAD attached to a branch? If so, which branch?
  • What is the hash ID of the current commit?

The git symbolic-ref HEAD command answers only the first question; git rev-parse HEAD mostly answers the second, but can be told to answer the first too / instead.


In point 1 above, the almost is there for a particular reason. Imagine you have just created a new, totally-empty repository. There are no commits in this repository, so which commit is the current commit?

This situation is problematic for Git. You're on a branch, namely master, that doesn't exist. Git calls this an orphan branch or a branch yet to be created (depending on which part of Git is doing the calling). The way Git handles this is to store the branch's name into .git/HEAD, without actually creating the branch itself in the reference database. When you make a new commit, that creates the branch itself, and now the problem is resolved: you're on the branch, which identifies the one new commit just made, which is the current commit, so HEAD names both the current commit and the current branch.

(Git can re-create this slightly distressed situation on demand, using git checkout --orphan, which writes a new branch's name into HEAD without actually creating the new branch.)

torek
  • 448,244
  • 59
  • 642
  • 775
  • The problem is that we do not want users to provide the origin/ prefix, like origin/some-branch, but simply write some-branch with origin/ implied. – Rihad Dec 15 '18 at 10:53
  • In that case, you fall right into the second category, with the adjusted premise: use the argument provided. – torek Dec 15 '18 at 11:04
  • The problem with the simple `git fetch && git checkout ` (where `branch` doesn't have origin/ prepended) is that in case of `commit` it works - the code on disk is the fetched one, but in case of `branch` the code on disk still remains unchanged, necessitating further steps like `git merge`. Well, all I want is the same number of steps for both branch & commit supplied, because it's impossible to tell them apart. – Rihad Dec 15 '18 at 11:05
  • So, it sounds like you want your script to: (1) run `git fetch`; (2) run `git checkout `; and (3) run `git merge --no-ff` or `git merge --ff-only` or `git merge`, relying on the upstream setting. (Which kind of merge to do, when, and why, depends on what kind of results you want.) Ultimately, it looks like you've asked the wrong question, perhaps by oversimplifying the actual problem. See [mcve] – torek Dec 15 '18 at 11:06
  • Would this work for both branch names & commit hashes? Hmm, let me try that, thanks. – Rihad Dec 15 '18 at 11:10
  • There will be no upstream when you have a detached HEAD, so `git merge` with no arguments will fail, but if you don't mind the failure, that's OK. – torek Dec 15 '18 at 11:10
  • Well, generally the problem is: download whatever updates remote has for this branch/commit. – Rihad Dec 15 '18 at 11:14
  • There's no such thing as *updates for a commit* as a commit is a specific frozen snapshot, identified by its hash ID. That hash ID is *that* commit, with *those* contents, forever. Note that this is quite separate from "what I have in my work-tree right now", which is not necessarily the same as *any* commit. – torek Dec 15 '18 at 11:36
0

This neat trick seems to do just what I want:

git show-ref --head --sha | grep -q ^argument

Its exit code will be 0 if the argument is a commit hash, 1 otherwise. Taken from here https://stackoverflow.com/a/29707340/3116571

Rihad
  • 99
  • 1
  • 2
  • 7
  • First, find out if the argument is a commit hash or not: `git show-ref --head --hash | grep -q ^argument` (unless we create a branch name the same as a commit hash, which is unlikely). So I could run `git fetch && git checkout $argument` then if it's a commit hash, don't do anything else, otherwise do `git merge origin/$argument`. How's that? – Rihad Dec 20 '18 at 06:00