4

Suppose a_branch is an existing branch that points to a different commit than the one HEAD points to (HEAD might point to a commit directly or via some branch).

Are the following commands equivalent?

git checkout a_branch

and

git symbolic-ref HEAD ref/heads/a_branch
git reset --hard a_branch


See also this related post.

Community
  • 1
  • 1
Evan Aad
  • 5,699
  • 6
  • 25
  • 36

2 Answers2

2

No, they're not, if you have staged or dirty changes before you run the commands.

If you modify a pre-existing file before running git checkout, the modification will persist after HEAD is moved. This is true whether the changes are staged or dirty.

The same does not hold when running git reset --hard. The modifications, staged or dirty, will be destroyed when you run git reset --hard.

Jordan Lewis
  • 16,900
  • 4
  • 29
  • 46
  • Thanks. Is this the only difference? – Evan Aad Apr 12 '17 at 17:41
  • 1
    Yes, I believe so. I believe the second command you mentioned, `git symbolic-ref HEAD ref/heads/a_branch`, is equivalent by itself to `git checkout a_branch`. – Jordan Lewis Apr 12 '17 at 17:45
  • 2
    No, that's not right. `git symbolic-ref` doesn't change file contents, like you probably already gathered - that's why you wanted to run the `reset` first. – Jordan Lewis Apr 12 '17 at 17:47
  • 1
    I think switching the order of those two commands *would* actually make them equivalent to `git checkout a_branch`. `git symbolic-ref HEAD ref/heads/a_branch` updates the `HEAD` pointer. Then, `git reset --hard a_branch` will discard the differences between the last state of your repo and the state of the repo at `a_branch`. – Jordan Lewis Apr 12 '17 at 17:49
  • Thanks, this helps a lot. Do you know why this equivalence breaks when `HEAD` and `a_branch` point to the same commit? – Evan Aad Apr 12 '17 at 17:55
  • I think this should work exactly the same if `HEAD` and `a_branch` point to the same commit, actually. – Jordan Lewis Apr 12 '17 at 17:57
  • It doesn't. To see it, try the following steps. (1) Create a file in the working directory, add it to the index, and commit. (2) Delete the file from the working directory and from the index, but don't commit. (3) Run `git checkout master`. The file is not recreated. Now run `reset --hard master`. The file is recreated. – Evan Aad Apr 12 '17 at 18:00
  • Oh, you meant on the `git checkout` side. Well, `git checkout` also preserves staged and unstaged modifications when switching branches. `reset --hard` will erase them - so there's another big difference between `checkout` and `reset` + `symbolic-ref`. – Jordan Lewis Apr 12 '17 at 18:04
  • 1
    I've rewritten my question as per your suggestion in your third comment above. Would you mind revising your answer in light of this new formulation and of the discussion we've had in the comments? – Evan Aad Apr 12 '17 at 18:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/141582/discussion-between-jordan-lewis-and-evan-aad). – Jordan Lewis Apr 12 '17 at 18:18
2

I also wondered, what git checkout (and git branch) really means in "plumbing" commands. Therefore, I tried one case, where I created a new branch, as it was only for learning the basics. To make sure, I don't miss anything, I started from scratch:

Create a folder "Test" with one file "test.txt" and write  
"Great content" into that file.

Using chapter 10 from the git book, I investigated the following sequence:

  • git init
  • git add .
  • git commit -m "Init"
  • EITHER porcelain commands:
    • git branch b1
    • git checkout b1
  • OR plumbing commands:
    • git symbolic-ref HEAD
    • git update-ref refs/heads/b1 $(git rev-parse HEAD)

The result: The two coincide!


The details. I will use the following commands to check the status:

Used commands from Git bash:

  • find .git/refs -type f find all files in .git/refs
  • git symbolic-ref HEAD points the symbolic HEAD reference, the same as cat .git/HEAD
  • git rev-parse HEAD get the history (Commits, objects?) reachable by HEAD
  • git rev-list --objects --all View all commit-objects
  • git cat-file -p refs/heads/master View the file content.
    If there were different branches with no common history, one has to use
    git rev-list --objects --no-walk $(git fsck --unreachable | grep '^unreachable commit' | cut -d' ' -f3)
  • git reflog shows one commit and the HEAD history (just one commit) Git porcelain commands
  • git branch -av Show all branches with hash and message (Also displays remotes if there are any!).

In the following, the commands are only shown if they result is something non-empty or it has changed since the last step.

Initialize Git repo

git init # creates empty .git/ folder. HEAD exists but without history.

No objects in .git/objects. Folders /info and /pack are empty.
.git/refs/ is empty

Stage changes

git add .
find .git/refs -type f # One object in c1/ created:
git cat-file -p c15479631b40176f3b09b7bc74ac5e189190e991 # yields "great content"
git cat-file -t c15479631b40176f3b09b7bc74ac5e189190e991 # yields  "blob"

Thus, one object in refs/objects/ was created

Commit changes

git commit -m "Init"
find .git/refs -type f # refs/heads/master created. Not empty. So have a look:
git symbolic-ref HEAD               # refs/heads/master
git cat-file -p refs/heads/master   # The full commit information including tree, author, commiter and commit message
git rev-parse HEAD                  # The hash to which HEAD points
git reflog                          # shows the HEAD history (just one commit)

# View all commit-objects. Everything is reachable by the commit:
git rev-list --objects --all        
git cat-file -p $(git rev-parse HEAD) # Commit Hash changes with time. -t instead of -p yields "commit"
git cat-file -p 69b13879c229e1cc35f270db248910e5a828dc65 # -t yields tree
git cat-file -p c15479631b40176f3b09b7bc74ac5e189190e991 # -t yields blob

git branch -av

master branch with hash from git rev-parse HEAD created

* master 7845459 Init

Create branch

git update-ref refs/heads/b1 $(git rev-parse HEAD)
# refs/heads/master still there. refs/heads/b1 created. So have a look:
find .git/refs -type f
git symbolic-ref HEAD          # Still at ref/heads/master
git cat-file -p refs/heads/b1  # The same result as from "git cat-file -p refs/heads/master"

git branch -av                      

b1 exists but not checked out:

  b1     7845459 Init
* master 7845459 Init

Checkout b1, i.e update HEAD

git symbolic-ref HEAD refs/heads/b1
git symbolic-ref HEAD  # now at refs/heads/b1 => b1 checked out! Cross-check:
git branch -av

b1 checked out:

* b1     7845459 Init
  master 7845459 Init

I hope, this is helpful.

Christoph
  • 6,841
  • 4
  • 37
  • 89