'git checkout' docs claim working tree will change; why are edits not discarded?
Asked Answered
P

3

28

I am new to Git and trying to wrap my head around the way branches work. According to the documentation, git checkout:

Updates files in the working tree to match the version in the index or the specified tree. If >no paths are given, git checkout will also update HEAD to set the specified branch as the >current branch.

So as I understand it, the files in my directory that I work in (the file I performed the git init in) would should change according to what branch I am in. I am confused, because this does not happen when I change between branches. Edits I was working on before I switched branches are present in the branch I switched to. Am I doing something wrong or does git checkout not work this way and I am just misunderstanding the documentation?

Perorate answered 24/6, 2014 at 11:3 Comment(1)
Sounds like uncommitted files what does git status show ?Altorelievo
D
22

Git has this general issue of cramming about eight or ten different things into one command. Note: Git 2.23 is splitting some of these up—helpful, to be sure, but also a very big change. (Should Git 2.23 be called Git 3.0? Git 2.0 changed the behavior of git add, which seems to me similar in degree.) See also VonC's answer.

git checkout can update the working tree, and usually does.

It can change where HEAD points, and sometimes does, sometimes doesn't.

It can overwrite work you did to a file, in case you want to reset the file and undo your work. Or it can refuse to overwrite work you did to a file, leaving it unchanged while either changing HEAD, or not changing HEAD.

The thing about all this is that, while it's remarkably difficult to describe, it actually all makes sense and after a while you get used to this and find that the one command does what you mean, most of the time. (It's that "most of the time" that can be a problem, of course....)

Anyway, the particular behavior you're seeing is a deliberate feature. Let's say you start out on branch main, as most repositories do:

$ git clone ...
$ git branch
* main
$

At this point you might edit some file(s), get some work going, and only then realize: "gah! I meant to do this on branch develop!"1

What Git lets you do at this point is switch to (or create) branch develop, keeping your modifications, under one condition: that switching to develop does not require wiping them out. Let's say you modified file f1 and created a new f2, and you now want to create and check out local branch develop that should start from, and automatically "track",2 origin/develop:

$ git checkout develop 

(in very old versions of git, you have to spell this git checkout -b develop --track origin/develop).

Let's also say that file f1 is the same at the tips of branch main and branch develop.3 What this means, to git, is that it can do this checkout, because it does not have to modify file f1, so it can leave your existing changes to f1 in place.

If file f2 is also the same in both commits, or (as in this case) does not exist in either one, then no files will be clobbered, and git checkout will create your new local branch develop, modifying the work tree to match origin/develop as needed—and this does not include modifying f1, nor removing f2, so the work you have done so far remains intact.

This allows you to commit your new changes to your local develop.

(If you run into cases where Git does have to undo your changes, but still want to "move" them to another branch, the usual trick is to use the git stash script. This sounds like a simple thing, and git stash is often simple to use, but it is actually quite a complicated little beast under the covers. Don't worry about that until you need it, though.)


1This happens to me all the time. Many times I want to create a new non-tracking branch, which is a bit simpler than switching to an existing branch, but the principle still applies.

2This automatic tracking allows you to more easily bring in changes other people have done: once Git picks them up with git fetch, Git will inform you about those other-people's-changes, and let you use git merge or git rebase to combine your changes with theirs, without a lot of extra poking-about to figure out whose changes go where.

3Since you're new to Git, concepts like distinguishing "the tip of a branch", which is a specific commit, from "the branch", which is actually ambiguous—there are branch labels, and then there are branch structures formed by the commit tree—is something else you mostly should ignore for a while. The main thing to note is that there's a special file in the Git repository named HEAD, and in that special file, git writes the string ref: refs/heads/main or ref: refs/heads/develop, in order to keep track of which branch you're on. So git checkout X will write ref: refs/heads/X into HEAD once it switches to branch X.

Meanwhile, another set of special files in the repository tell Git that branch main refers to one of those big ugly SHA-1s like c06f8d11b75e28328cdc809397eddd768ebeb533. This is the "tip" of branch main. When you make a new commit on main, Git creates the new commit "one past the old tip", then writes the new SHA-1 into the branch files, so that main is now your new commit.

The precise details don't matter so much as the idea that new commits simply advance the branch-tip.

Dialectal answered 24/6, 2014 at 11:32 Comment(0)
S
75

That confusion is acknowledged by Git 2.23.
Git 2.23 (Q3 2019) will replace git checkout with two new commands:

See commit 97ed685, commit d16dc42, commit bcba406 (20 Jun 2019), commit 4e43b7f, commit 1235875, commit 80f537f, commit fc991b4, commit 75f4c7c, commit 4df3ec6, commit 2f0896e, commit a5e5f39, commit 3a733ce, commit e3ddd3b, commit 183fb44, commit 4058199, commit a6cfb9b, commit be8ed50, commit c9c935f, commit 46e91b6 (25 Apr 2019), and commit 328c6cb (29 Mar 2019) by Nguyễn Thái Ngọc Duy (pclouds).
(Merged by Junio C Hamano -- gitster -- in commit f496b06, 09 Jul 2019)

checkout: split part of it to new command 'switch'

"git checkout" doing too many things is a source of confusion for many users (and it even bites old timers sometimes).
To remedy that, the command will be split into two new ones: switch and restore. The good old "git checkout" command is still here and will be until all (or most of users) are sick of it.

And:

switch: reject if some operation is in progress

Unless you know what you're doing, switching to another branch to do something then switching back could be confusing. Worse, you may even forget that you're in the middle of something. By the time you realize, you may have done a ton of work and it gets harder to go back.

A new option --ignore-in-progress was considered but dropped because it was not exactly clear what should happen.
Sometimes you can switch away and get back safely and resume the operation. Sometimes not.
And the git-checkout behavior is automatically clear merge/revert/cherry-pick, which makes it a bit even more confusing.
See this discussion.

We may revisit and add this option in the future.
But for now play it safe and not allow it (you can't even skip this check with --force).
The user is suggested to cancel the operation by themselves (and hopefully they do consider the consequences, not blindly type the command), or to create a separate worktree instead of switching.

The third option is the good old "git checkout", but it's not mentioned.


See git switch man page

DESCRIPTION

Switch to a specified branch.
The working tree and the index are updated to match the branch.
All new commits will be added to the tip of this branch.

Optionally a new branch could be created with either -c, -C, automatically from a remote branch of same name (see --guess), or detach the working tree from any branch with --detach, along with switching.

Switching branches does not require a clean index and working tree (i.e. no differences compared to HEAD).
The operation is aborted however if the operation leads to loss of local changes, unless told otherwise with --discard-changes or --merge.


EXAMPLES

The following command switches to the "main" branch:

$ git switch main

After working in the wrong branch, switching to the correct branch would be done using:

$ git switch mytopic

However, your "wrong" branch and correct "mytopic" branch may differ in files that you have modified locally, in which case the above switch would fail like this:

$ git switch mytopic
error: You have local changes to 'frotz'; not switching branches.

You can give the -m flag to the command, which would try a three-way merge:

$ git switch -m mytopic
Auto-merging frotz

After this three-way merge, the local modifications are not registered in your index file, so git diff would show you what changes you made since the tip of the new branch.

To switch back to the previous branch before we switched to mytopic (i.e. "main" branch):

$ git switch -

You can grow a new branch from any commit.
For example, switch to "HEAD~3" and create branch "fixup":

$ git switch -c fixup HEAD~3
Switched to a new branch 'fixup'

If you want to start a new branch from a remote branch of the same name:

$ git switch new-topic
Branch 'new-topic' set up to track remote branch 'new-topic' from 'origin'
Switched to a new branch 'new-topic'

To check out commit HEAD~3 for temporary inspection or experiment without creating a new branch:

$ git switch --detach HEAD~3
HEAD is now at 9fc9555312 Merge branch 'cc/shared-index-permbits'

If it turns out whatever you have done is worth keeping, you can always create a new name for it (without switching away):

$ git switch -c good-surprises

Note the error messages that "git switch" mentions its option to create a new branch, "-b/-B" options were shown, where "-c/-C" options should be, which has been corrected with Git 2.27 (Q2 2020).

See commit 7c16ef7 (30 Apr 2020) by Denton Liu (Denton-L).
(Merged by Junio C Hamano -- gitster -- in commit f4675f3, 08 May 2020)

switch: fix errors and comments related to -c and -C

Reported-by: Robert Simpson
Signed-off-by: Denton Liu
Reviewed-by: Taylor Blau

In d787d311db ("checkout: split part of it to new command 'switch'", 2019-03-29, Git v2.23.0-rc0 -- merge listed in batch #4), the git switch command was created by extracting the common functionality of cmd_checkout() in checkout_main().

However, in b7b5fce270 ("switch: better names for -b and -B", 2019-03-29, Git v2.23.0-rc0 -- merge listed in batch #4), the branch creation and force creation options for 'switch' were changed to -c and -C, respectively.

As a result of this, error messages and comments that previously referred to -b and -B became invalid for git switch.

For error messages that refer to -b and -B, use a format string instead so that -c and -C can be printed when git switch is invoked.

Suppliant answered 16/7, 2019 at 22:23 Comment(4)
Thanks for the excursion into the development of git and the brief explanation of the new commands. Just wondering how long it will take that the advantages will spread in the world. I'm thinking eg. of LTS-versions of linuxes or yocto that uses git as buildtool in its build chain. Newest version of git now is 2.31? If they were brave enough they would have just removed the checkout command by now :)Contrariwise
@Contrariwise I don't think they will remove the checkout command, but strongly encourage the use of switch/restore.Suppliant
I am kind of more confused about these new commands than I was with checkout. How do you translate git checkout HEAD^1 into switch/restore?Mauromaurois
@Mauromaurois That is the point of those two new commands: forcing you to tell Git what you want. Restore the working tree with the content of the last commit? git restore @^1 (which would have been git checkout @^ -- .). Switching to a commit: git switch @^, which will not work by the way, not unless you add --detach, making crystal clear that you get a detached HEAD mode (as opposed to git checkout @^, which silently detached your HEAD to a commit instead of a branch). See the last part of "Why did my Git repo enter a detached HEAD state?"Suppliant
D
22

Git has this general issue of cramming about eight or ten different things into one command. Note: Git 2.23 is splitting some of these up—helpful, to be sure, but also a very big change. (Should Git 2.23 be called Git 3.0? Git 2.0 changed the behavior of git add, which seems to me similar in degree.) See also VonC's answer.

git checkout can update the working tree, and usually does.

It can change where HEAD points, and sometimes does, sometimes doesn't.

It can overwrite work you did to a file, in case you want to reset the file and undo your work. Or it can refuse to overwrite work you did to a file, leaving it unchanged while either changing HEAD, or not changing HEAD.

The thing about all this is that, while it's remarkably difficult to describe, it actually all makes sense and after a while you get used to this and find that the one command does what you mean, most of the time. (It's that "most of the time" that can be a problem, of course....)

Anyway, the particular behavior you're seeing is a deliberate feature. Let's say you start out on branch main, as most repositories do:

$ git clone ...
$ git branch
* main
$

At this point you might edit some file(s), get some work going, and only then realize: "gah! I meant to do this on branch develop!"1

What Git lets you do at this point is switch to (or create) branch develop, keeping your modifications, under one condition: that switching to develop does not require wiping them out. Let's say you modified file f1 and created a new f2, and you now want to create and check out local branch develop that should start from, and automatically "track",2 origin/develop:

$ git checkout develop 

(in very old versions of git, you have to spell this git checkout -b develop --track origin/develop).

Let's also say that file f1 is the same at the tips of branch main and branch develop.3 What this means, to git, is that it can do this checkout, because it does not have to modify file f1, so it can leave your existing changes to f1 in place.

If file f2 is also the same in both commits, or (as in this case) does not exist in either one, then no files will be clobbered, and git checkout will create your new local branch develop, modifying the work tree to match origin/develop as needed—and this does not include modifying f1, nor removing f2, so the work you have done so far remains intact.

This allows you to commit your new changes to your local develop.

(If you run into cases where Git does have to undo your changes, but still want to "move" them to another branch, the usual trick is to use the git stash script. This sounds like a simple thing, and git stash is often simple to use, but it is actually quite a complicated little beast under the covers. Don't worry about that until you need it, though.)


1This happens to me all the time. Many times I want to create a new non-tracking branch, which is a bit simpler than switching to an existing branch, but the principle still applies.

2This automatic tracking allows you to more easily bring in changes other people have done: once Git picks them up with git fetch, Git will inform you about those other-people's-changes, and let you use git merge or git rebase to combine your changes with theirs, without a lot of extra poking-about to figure out whose changes go where.

3Since you're new to Git, concepts like distinguishing "the tip of a branch", which is a specific commit, from "the branch", which is actually ambiguous—there are branch labels, and then there are branch structures formed by the commit tree—is something else you mostly should ignore for a while. The main thing to note is that there's a special file in the Git repository named HEAD, and in that special file, git writes the string ref: refs/heads/main or ref: refs/heads/develop, in order to keep track of which branch you're on. So git checkout X will write ref: refs/heads/X into HEAD once it switches to branch X.

Meanwhile, another set of special files in the repository tell Git that branch main refers to one of those big ugly SHA-1s like c06f8d11b75e28328cdc809397eddd768ebeb533. This is the "tip" of branch main. When you make a new commit on main, Git creates the new commit "one past the old tip", then writes the new SHA-1 into the branch files, so that main is now your new commit.

The precise details don't matter so much as the idea that new commits simply advance the branch-tip.

Dialectal answered 24/6, 2014 at 11:32 Comment(0)
P
1

When you create a branch, that branch will automatically get the files of the branch you were in when you created this new branch.

Let's say you are in main branch and you want to create a develop branch. All together should look like this:

git checkout -b develop    # create and switch to develop branch
touch text.txt             # create a file
git add .                  # add file to staging area
git commit -m "adding text.txt" 
git checkout main

And then you won't see text.txt since you are in main.

Pontoon answered 24/6, 2014 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.