I have been learning all about .gitignore file but there's a question that I would want to solve. .gitignore should contain all files that you want to ignore. Therefore, you should ignore files generated by the OS, the IDE you are working on... My question appears when the repository is on Github and people can clone it and push the changes. These people can use other operating systems and can use other IDEs. Thus, gitignore should ignore the files generated by these others OS and IDEs.

What should you do? Do you have to write in gitignore all the files generated by all the operating systems and generated by all the IDEs?

2

3 Answers

There are two background points I want to emphasize right off:

  1. If you own the repository, you set the rules. Anything you do to accommodate other people is just general friendliness.

  2. The verb ignore is ... tricky, at best. I'll describe what I mean later. The important thing is that listing a file in .gitignore does not exactly ignore it, unless you have an odd personal definition for the word "ignore".

That said, the way to be friendly is to have your repository ignore only files that your project will produce. Then, have your personal ignore file ignore files that your system will produce.

Let's use a concrete example. Suppose you have a project with Python, where running python foo.py creates foo.pyc, foo.pyo, and/or __pycache__/* files, none of which should ever be committed. You'd therefore start with:

*.pyc *.pyo __pycache__/ 

in your .gitignore, because anyone using your project—you, or your co-workers, or anyone else—will end up with these Python "object code" files, which are specific to a particular Python version and hence should not be included.

But suppose you personally are using MacOS and its Finder. The Finder program creates files named .DS_Store. So you could well be tempted to add:

.DS_Store 

to your .gitignore. That's not wrong, but it doesn't do anyone using Windows any good. What files do the Windows folks need to ignore? I'm not sure, I don't use Windows. The Linux folks, however, might like to ignore .*.swp files, which the vim editor creates.

If you put .DS_Store in your own $HOME/.gitignore, and the Linux folks put .*.swp in their $HOME/.gitignore, all of you will have a happy experience with your project. Moreover, you'll have a happy experience with their project, in which they didn't list .DS_Store because they started on Linux.

So that's the general idea: your project (repository) .gitignore should list the names or name-patterns of files that will be found in work-trees when working with your project, but that should not be committed to the project. In other words, it's not OS-specific, it's project-specific. Other file name patterns—OS-specific, editor-specific, IDE-specific, and so on—can go in other ignore files, and hence need not be listed in the project's .gitignore files. It does not necessarily hurt to list them in the project's files, but if each person is sensible about things, it doesn't help either.

Less-important background that is not part of the actual answer (you can stop reading here!)

People finds Git's .gitignore files confusing. (I did, and judging from the hundreds of questions here on StackOverflow, pretty much everyone does.) I think a big chunk of this comes from misunderstanding Git's storage model.

The first thing to know about Git—probably the most important thing to know—is that Git isn't about files, and isn't really about branches either. Git is really all about commits. A Git repository, at its heart, consists of two databases. The big database holds commits and other internal Git objects needed to support the commits.

This big database, of Git commits and other Git objects, is what git clone copies. There's a second, smaller, database of names: branch names, tag names, and so on. This database is visible to other Gits, so it could be copied by git clone, but usually it isn't merely copied. Instead, git clone reads that smaller database and modifies it, throwing away some names entirely and changing others. So when you use git clone, you get a copy of the big database (all the commits) and a modified and sawed-down copy of the small database. (We won't look too closely at the smaller one here since it doesn't affect .gitignore files.)

The commits themselves all have unique hash IDs. These are big ugly strings of letters and digits, such as b994622632154fc3b17fb40a38819ad954a5fb88. A Git repository can quickly tell if it has the same commits as some other Git repository: the sending Git just lists the hash IDs. The receiving Git just checks: Do I have a commit with that hash ID? If so, the receiving Git has that commit. It doesn't need to get it again. If not, the receiving Git needs to get that commit.

This means your first git clone might be slow: you might have to get many megabytes of objects. After that, though, updating a clone is just a matter of getting any new commits they have that you still need. Your Git calls up their Git, they list some hash IDs, and your Git knows what to get and their Git knows what you have. Or, if you've made new commits to give to them, your Git calls up their Git, offers them some hash IDs, and they can say I have that one already or I don't have that one, gimme!

There is a bit more to it than this, of course. The next thing to know is that every commit stores a full and complete snapshot of every file. These files are stored in a special, read-only, Git-only, frozen format, in which the files are de-duplicated. The fact that commits store files is how Git, which is really only concerned with the commits themselves, winds up storing files for us. The frozen and de-duplicated format is why repositories do not grow enormously fat, even though every commit has a full copy of every file: most commits just re-use the files from the previous commit, which means Git doesn't have to store a new copy.

But if the file inside a commit are in a frozen, Git-only format, that no other program on your computer can use, how will you ever actually use these files? The answer is: you won't. That is, you won't use these files. What Git will do is extract these files somewhere. That "somewhere" is your working tree or work-tree.

It's worth mentioning here, though we won't go further into it, that each commit stores not just a frozen snapshot, but also some extra metadata. This is mostly the stuff you see in git log output: who made the commit, when, and why, for instance. The why part is up to the person who makes the commit: it's the log message. A good log message is worth a great deal. Git can tell you what happened: Git will compare the previous, or parent, commit's snapshot against the current or child commit's snapshot, and for every file that's different, Git will show you a recipe that changes the parent's copy into the child's copy. But Git can't tell you why some line was added or deleted. Only the person who did that can say why they did that.

This means that the files you see and work with are not in Git at all

If you've run:

git clone 

and have a copy of Git, you can look around at the source for Git: there's a Makefile, a README.md, and so on. But these are ordinary files on your computer. They're not files in a commit. They are copies that Git made by extracting committed files from a snapshot. These copies are in your working tree or work-tree. You can see them with file viewers, open them in editors, and so on. But they're not in Git. They are in your work-tree, for you to do with as you like.

Git will extract any given commit to your work-tree any time you ask it to:

git checkout v2.21.0 

for instance will use the tag v2.21.0 to find a particular commit hash ID (8104ec994ea3849a968b4667d072fedd1e688642, to be exact) and extract that commit to your work-tree. (If you have a Git that is 2.23 or later, you can use git switch instead of git checkout: these do exactly the same thing here.) This extraction process consists of removing your files from your work-tree and creating new ones based on the commit you're switching to. But all these files are your files, not Git's.

Fortunately, git checkout / git switch has some safety checking to avoid deleting your files when you haven't saved some changes you have made. You can turn this off (git checkout --force, for instance) or use other destructive commands on purpose (git reset --hard) to erase unsaved work. In all cases you're basically just telling Git to erase the stuff you did to your file and get some other version back, such as the version saved in some other commit, from Git's files.

Git's index or staging area

If Git used just two things—its commits, of which one is the current commit, and your work-tree—then git commit itself would be simple. Unfortunately, Git hides a third place to keep each file. When you select some commit—via git checkout or git switch—to be the current commit, Git doesn't just extract that commit's snapshot to your work-tree. Instead, it first extracts that commit's snapshot to Git's index.

The index is complicated and has multiple purposes, but its main one is actually pretty easy to describe, and is what you should remember to start with: The index is where you build the next commit you plan to make. This is why it has the name staging area. The index holds a copy1 of each file, initially taken from the commit. Your work-tree holds a copy as well. So there are three active copies:

  • The one you can see with git show HEAD:README.md is frozen into the commit.
  • The one you can see with git show :README.md is in Git's index. It's in the frozen format, but it is replaceable, unlike the one in the commit. (These files are sort of half-in Git: ready to be committed, but not actually committed yet.)
  • The one you can actually use—that's in a plain file—is just plain README.md. This is yours and it's not in Git at all.

When you run git commit, Git gathers up the appropriate metadata, freezes whatever files are in its index right then, and uses those to be the new snapshot for the new commit.

If the :README.md matches the HEAD:README.md, these two files are duplicates, so the new commit just re-uses the file. If not, maybe it matches some other commit and de-duplicates that way, or maybe it's all new, and actually gets stored for real. In any case, once you commit it, it's frozen and really, fully in Git now. But if you've changed your work-tree copy of README.md, you probably want Git to freeze the updated README.md. This is where git add comes in.

The git add command tells Git: Make the index copy match my work-tree copy. That is, Git will copy (and compress into the frozen format) your updated README.md file from your work-tree, and put the copy into :README.md in its index. So this is why you are constantly required to git add files: every time you've changed your copy, if you want Git to change its proposed next commit copy, you must git add again.

When you run git commit, later, Git will take all the index files and freeze them into a new commit. Because the index copies are all in the frozen format, this process can and normally does go very fast.


1Technically the index contains, not an actual copy of the data, but rather the file's name, mode, and a blob hash ID. You can't really tell the difference unless and until you start digging into the index directly, using git ls-files --stage or git update-index. So it works OK to think of the index as having a full copy of the file: Git hides the blob-object trick so well that you don't need to care.


This is where .gitignore comes in

Git makes new commits from its index, not from your work-tree. Your work-tree is yours, to do with as you will. You just need to be a little careful when you tell Git to overwrite it, since none of the files in your work-tree are in Git (they are at most next to or along the side of Git). But this also means you can create files in your work-tree that you never want Git to store into any of its commits. Since these files aren't in commits, and it's only the commits that get copied by git clone, these files won't appear in any clone.

For compiler output files like *.pyc, or *.o from cc or c++, or output from a Java compiler, or whatever, that's a good thing: you don't normally want these files to appear in any clone.

But if these files are just lying around in your work-tree, two things can go wrong:

  1. git status will nag you about them.
  2. If you use an en-masse git add everything operation, git add will copy these files into Git's index as new files, and now they'll get committed if you git commit.

Listing a file name in .gitignore is a way to prevent both of these things. But there's a trick here: If a file is already in Git's index, listing it in a .gitignore has no effect.

Files that are in Git's index are called tracked. A tracked file is one that is in Git's index right now. An untracked file is one that exists in your work-tree, but is not in Git's index right now.

Remember, you can put all-new (to Git) files into Git's index right now with git add. You can also take files completely out of Git's index right now with git rm. So the index's contents are not fixed. A git checkout fills the index, and then after that, you can—and will—modify it: you'll replace any files you want updated in the next commit.

When you run git status, the status command makes two separate comparisons. First it tells you other useful stuff, but we'll skip right over that and get to the two comparisons:

  1. The first of the two comparisons compares the current commit, or HEAD, to what is in the index. For each file that exactly matches, git status says nothing. If there are some files that don't match—or are new or are missing—git status says changes staged for commit and lists these files' names.

  2. The second comparison compares the index to your work-tree. For each file that exactly matches, git status says nothing. If there are some files that don't match, or are missing, git status says changes not staged for commit and lists these files' names.

The one special case here is for untracked files: for each untracked file, git status lists the file's name,2 calling these untracked files. But if you list these names in .gitignore, git status shuts up about them.

Note that nothing special happens for tracked files. Those are already in Git's index. They're covered by the first comparison, and Git will compare the index copy to the work-tree copy, regardless of whether the file is listed in a .gitignore.

So in this sense, these .gitignore entries do not mean ignore the file. They mean shut up about the file when it is untracked. When it's tracked, they have no effect.

Meanwhile, git add has . and * (among others) for doing en-masse add operations on many or all files. If all files included untracked files, these operations would be highly inconvenient. So listing file names or patterns in .gitignore suppresses en-masse add operations. It even suppresses a deliberate git add:

$ touch foo.pyo $ git add foo.pyo The following paths are ignored by one of your .gitignore files: foo.pyo Use -f if you really want to add them. 

So perhaps .gitignore should have been called .git-do-not-complain-about-these-untracked-files-and-do-not-automatically-add-them-when-using-en-masse-add-operations-or-even-explicit-requests, or something like that. But who wants to type in that kind of name? So .gitignore it is.


2Technically, to get this every time you need git status -uall or git status -u. Otherwise it will sometimes combine a bunch of files that are physically stored within a single folder and only bother mentioning the folder name.

In general, the things you put in .gitignore on a typical project include anything that your build system might produce, plus any configuration files that are user-specific (say, if your program requires the user to generate a config file to run). If there are other build products or generated files that users might create in development (say, HTML files from Markdown or AsciiDoc) but that aren't normally built, you should ignore those, too.

If your project is such that everyone must use the same IDE or OS (say, your project only compiles with Visual Studio or on macOS and nobody will ever use another IDE or OS), then you can put editor- or OS-specific files in there as well.

People can have their own ignore files on their own systems (via core.excludesFile), so if a user uses Vim, they should adjust their own per-user ignore files such that they ignore swap files. Similarly, macOS users should ignore .DS_Store. You aren't responsible for dealing with every operating system or editor that someone might use. You may choose to use a pre-created gitignore file that covers some of these as part of it, but are under no obligation to do so.

Having said that, typically projects perform code review, so if a user has not configured Git appropriately on their system and checked in an unsuitable file, the reviewer can just ask them to fix it and adjust their Git configuration. This is typically the approach used by most large projects and works pretty well.

repository is on Github and people can clone it and push the changes

This is where you put in some sort of quality gates, like code reviews. They are meant for discussions, and having some other pairs of eyes go over the changes. Looking over the diff, you notice other stuff that's not useful, e.g. IDE files. Then, you kindly ask the contributor to remove those and resubmit.

In the case of most OSS, I'd think, the maintainer has a repository, which is cloned/forked by contributors, and when they want to bring changes in, they make a PR. Because you usually wouldn't want anybody to make changes to your code, you restrict the write rights to who you trust, so others can't push directly to the main repo.

In the case of smaller projects, where you know all the contributors, there's still a chance to accidentally bring in unwanted files, and that's still why you would want something like a code review before merging in the mainstream.

Either way, it's a process issue, and not necessarily a git one. Depends on the case. And like any other repetitive work, when you notice a pattern, automate it.

You're right in feeling a bit scared about having to consider all the systems of anybody who could contribute, but you don't have to.

  1. I think most languages have code linters, so you can enforce a coding style (e.g. tabs vs spaces).
  2. Also, you usually know what files can be produced by a language, e.g. .exes, .dll, so you can add those to your .gitignore file.
  3. For anything that slips through, there's pull requests.

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy