Home > Software design >  Don't remove local file which is in .gitignore while change branch
Don't remove local file which is in .gitignore while change branch

Time:09-23

I have a branch named A and I created some files and folders which added to the .gitignore file before pushing them into repo.

I wanna keep them in my editor while I change to the new branch B which doesn't contain these files and folders.

Can anyone help me how to achieve this goal?

CodePudding user response:

Listing a file in .gitignore has no effect on whether Git will remove it. That's all there is to say about that, but the rest of your question displays a great deal of confusion about how Git works:

  • Files are not stored in branches. Files are stored in commits.

  • Files are not "pushed into a repo". A repo, or repository, is a collection of commits. Those commits then contain files. Branches—or more precisely, branch names—help you (and Git) find specific commits that you think are particularly interesting for some reason. Usually that reason is "because this is the latest commit on that branch", but it is important to keep in mind that the branch name is merely locating that particular commit, and also providing one more service (see below).

  • Files you have in your working tree are not actually in Git. Files that are in your working tree and are open in your editor are being handled by your OS and your editor.

  • Checking out a commit, with git checkout or git switch, means select a commit to become the current commit. For a commit to be the current commit, Git needs to extract all of its files. These files, which are in the commit, are copied into two places:

    • The files get copied into Git's index AKA the staging area. These are two names for the same thing. (It even has one more name, the cache, although this is mostly outdated now.)

    • The files also get copied into your working tree.

    We'll say more about each of these in a moment.

    To switch from the current commit to some other commit, Git must remove, from Git's index AKA staging area, each of the files that is there now. As Git removes each such file, it also removes the copy in your working tree.

So the files in your working tree that will be removed by a successful git checkout or git switch are the files that are in Git's index. Those files will then be replaced by the files that come out of the commit you're switching to, which then becomes your current commit: now your index and working tree match the new current commit, instead of the old current commit.

Your working tree is yours, to do with as you will. Certain Git commands, including git checkout and git reset for instance, tell Git to do things to your working tree files, but at all times, your working tree is an ordinary directory (or folder, if you prefer that term) on your computer, holding ordinary files. Every ordinary computer program that works with ordinary files works with these files.

The files that are stored inside Git commits are special: they are read-only, compressed, and de-duplicated. Only Git can read them, and literally nothing—not even Git itself—can overwrite them. The de-duplication aspect is important, because every commit stores a full copy of every file. But if you have 3000 files, and you make 1000 commits in a row, changing only one file each time, each commit simply re-uses 2999 earlier files. So the 1000 new commits do not add 3000 more files every time: they only add one file, or maybe zero files. (A new commit that turns a file back to the way it appeared earlier can wind up re-using the old files from one or more earlier commits, and hence use no space at all to store the files—except for some minor overhead, that is.)

This special storage format means that Git's commits act as permanent archives. You can always get every older version back, provided you have the commits. But it also means that they're completely useless for getting any new work done because they're read-only! (And even then, only Git can find and read them: they're in the .git/objects directory, in unrecognizable forms called loose objects and packed objects.)

This is why Git must extract the files from the commit. The extracted files, in normal everyday form, are there in your working tree, so that you can see them and work on them.

This does not, however, explain the need for Git's index. In fact, other version control systems don't have an index / staging-area (or if they do have something like it—most really do, internally—they keep it secret so that you don't have to know about it). But Git's original author, Linus Torvalds, chose to force you to learn about the index. You really do have to: you can get away without learning about it for a while, but not knowing about it will bite you in the butt eventually, because of things like this "which files get removed" question.

So: What exactly is the index? It has multiple roles, and it is used for handling merge conflicts, for instance. But the primary role, and the one we're concerned with here, is that Git's index holds your proposed next commit. Inside the index, Git has stored every file that will go into the next commit. These files are pre-compressed and pre-de-duplicated, so that they're ready to go. Because they are pre-de-duplicated, they start out taking up no space: they're copies of what's in the commit, so they're duplicates, so they're de-duplicated away.

When you run git add to update a file in Git's index, Git will, at this point, read the working tree copy. Git will compress it, getting it ready to become one of Git's internal objects, and check to see if it's a duplicate. If it is a duplicate, Git updates the index copy to refer to the duplicate. If it's not a duplicate, Git updates the index copy to refer to the new ready-to-commit version. Either way, the file is now ready to commit. The proposed next commit is now different because you made one (or many) files in Git's index match the corresponding files in your working tree.

This means that at all times, Git's index holds your proposed next commit. (When there are conflicts, this proposed next commit is marked "can't commit this", so it's perhaps an overstatement to say that it's the proposed next commit at this time. But it's still what would be committed, if you could commit.)

When you do finally run git commit, Git:

  • packages up all the files that are in its index: this will be the new snapshot;
  • adds metadata, such as your name and email address and the current date and time, to use in the new commit to make;
  • actually makes that new commit, with the current-at-the-moment commit as the parent of this new commit; then
  • switches to the new commit by writing the new commit's new, unique hash ID into the current branch name.

This means that the act of committing makes a new commit that becomes the current commit. It makes that commit from the contents of Git's index / the-staging-area / your proposed next commit: your proposed next commit now is a commit, and the commit and the index / staging-area match, just like they normally do when you first check out some commit. This is the special feature of a branch name: making a new commit while being "on" that branch automatically updates the hash ID in the branch name, so that it continues to be the latest commit on that branch.

To remove a file from Git's index without changing commits, you use git rm. Note that this removes the file from Git's index and from your working tree, so that it's gone from both places. You can, however, use git rm --cached: this instructs Git to remove the file from Git's index, but not from your working tree. Either way, the proposed next commit no longer has that file.

This brings us to our list of cases: times when Git won't remove a file from your working tree, whether or not you have it open in some editor.

Non-removal cases

Remember, the fundamental idea about switching from the current commit to some other (target) commit is that Git will:

  • remove, from its index and your working tree, files from the current commit;
  • insert, into its index and your working tree, files from the target commit.

Since Git has, and exposes, its index, the way Git knows which files came out of the current commit is to look in its index. This means you can stop Git from removing a file, even if it really did come out of this commit, by removing it from Git's index, without first removing it from your working tree:

git rm --cached file.ext

Now that the file isn't in Git's index, Git says that the working tree version is an untracked file. This is, in fact, the definition of an untracked file. An untracked file is a file that exists in your working tree, but not in Git's index.

This is the simplest method, but it has several caveats:

  1. You must remember to run git rm --cached every time it's necessary. If anything—such as a successful git checkout—puts a copy of file.ext into Git's index again, now it's a tracked file.

  2. You must remember that if the file is in some commit, checking out that commit will put the file back into Git's index. This makes step 1 necessary.

  3. Everyone else must remember all of this too: if they have a working tree, with some commit checked out, their Git has an index keeping track of their working tree files. If they switch from this commit to some other commit, their Git will remove their working tree copy as well. This is just rules 1 and 2 restated from their point of view, but it's crucial to keep it in mind: just because you know to run git rm --cached doesn't mean they know to do that.

There's another special case here. Suppose you ask Git to switch from commit a123456, for instance, to commit b789abc. Now, each of these two commits contains an archived set of files. Suppose further:

  • a123456 (the current commit) has a file.ext in it.
  • b789abc (the target commit) also has a file.ext in it.
  • Moreover, the two copies in those two commits are exactly the same.

In this case, switching from one commit to the other doesn't really require that Git remove the current one and put in the other one. That would, in fact, be wasted effort, if the file in your working tree matches the file in Git's index and the file in the current commit.

So Git doesn't bother. And, because it doesn't bother, this means that you can switch from one commit to another, even if the file is "modified" in your working tree. The details here get pretty sticky; if you want to read up on them, see Checkout another branch when there are uncommitted changes on the current branch.

Removal doesn't bother some editors

Some editors are smarter and/or less-annoying than others.

Suppose you open a file in your working tree and begin editing it. Then something or someone—Git or not—swings by and, for some reason, deletes or otherwise changes the file. But your editor has a copy of the file open!

Your editor may notice that the file got deleted or changed. If so, it might automatically throw out all the work you did without giving you a chance to save it. This is extremely unfriendly and you should stop using that editor, if possible. Then you'll have an editor where, if the file is removed, you can still save it somewhere (inside or outside your working tree; that part is up to you and your editor).

More about .gitignore

The .gitignore file is not a listing of files to be ignored. As such, it's pretty clearly mis-named. But the right name would be unwieldy.

We've already mentioned that an untracked file is a file that exists in your working tree, but isn't in Git's index right now. How did this come about? Perhaps Git extracted the file from the current commit, and then you ran git rm --cached. Perhaps the file isn't in the current commit, and you created it recently yourself and have not run git add. However it came about, it is true now: the file exists in your working tree, but not in Git's index, so it is untracked. Running git status will list this file's name, as an untracked file.

The git status command is supposed to be helpful. If it lists 50,000 untracked files every time you run it, so that any useful information is impossible to find, what good is that? So, Git has a way to make git status shut the f— up about some untracked files. To do that, you list these files' names, or "glob patterns" like *.o or *.pyc, in a .gitignore file. The status command then shuts up about these.

The git add command is supposed to be convenient to use. You can run git add . to add every file that's in the current working directory in your working tree, and all files within folders within that directory too. But what if there are a bunch of untracked files here that should stay untracked? This isn't so helpful. So, to tell git add not to automatically en-masse add certain existing untracked files, you list these files' names, or glob patterns, in a .gitignore file. The add command then does not auto-add these.

But if a file that is nominally .gitignore-ed is already in Git's index, Git will not ignore it. Hence, the .gitignore file isn't really files to ignore. It should be named .git-do-not-complain-about-these-files-if-they-are-untracked-and-if-they-are-untracked-do-not-auto-add-them-with-en-masse-git-add-operations-either-because-they-should-stay-untracked, or something along these lines. But that name is ridiculous, so .gitignore it is.

The bottom line

In the end, switching from commit C1453 (or whatever) at tip-of-branch-A to commit C9782 (or whatever) at tip-of-branch-B is going to update Git's index. So what you care about is what's in Git's index now, vs what is in the commit at the tip of branch-B. As it updates Git's index, it is also going to update working tree files.

The git checkout and git switch commands are generally pretty nice about this: if you have uncommitted work in some of these files, and Git would remove them (and thereby clobber your uncommitted work), Git will tell you that and abort the checkout/switch operation. Git will remove the files without complaining if they're committed. Since commits are permanent (mostly—with a great deal of work, you can sometimes get rid of some commits) and read-only (entirely), if you need the files back after switching, just tell Git to extract one or more files from that other commit.

Note that Git also allows you to create additional working trees. Each newly added working tree comes with its own separate index / staging-area. This allows you to have two different commits, from two different branch names, checked out into two different working trees. One working tree will be on branch-A (as git status there will show), and one will be on branch-B.

CodePudding user response:

I try to answer you point per point.

I have a branch named A and I created some files and folders which added to the .gitignore file before pushing them into repo.

You are stating that you ignored some files and folders before pushing to remote, so you have them as untracked locally. Is it right?

I wanna keep them in my editor while I change to the new branch B which doesn't contain these files and folders.

If what I stated in the previous point answer you have some untracked items. In that situation you can freely switch branch without loosing them, thus keeping them opened into the editor.

Can anyone help me how to achieve this goal?

If my guesses are right, you should simply issue git checkout B.

Let me know it it is all right.

Edit:

Try again with:

git stash save -u
git checkout B
git stash pop
  • Related