1

enter code hereThe directory looks like this:

> Repo
    LICENSE
    README.md
    .gitignore
    > App
        'contents of application all in here'
        .gitignore

Our .gitignore file comes from here https://github.com/expo/expo/blob/master/.gitignore .

Not sure where to put the .gitignore file though because in all the projects I've used they've been in the repo folder directly, but wouldn't the .gitignore then need to have App/_____ in front of all the files listed?

Thanks

Antonio Petricca
  • 8,891
  • 5
  • 36
  • 74
chung
  • 835
  • 1
  • 7
  • 19
  • Not really an expo question so much ad a git question. Put it in root of the entire folder space you want versioned. Gitignore doesn't always need prefixes of the path to match files and folders. – Atmas Aug 31 '21 at 04:54

1 Answers1

1

TL;DR

There is no single right answer. Put the entries wherever it works best for you.

Long

Regardless of the application, the rules for .gitignore are consistent (though quite complex). You will need to understand the syntax and semantics of each entry in an exclusion file, but first and foremost, before you even get to that point, you must understand what it means, in Git, for a file to be tracked vs untracked, because exclusion entries apply to untracked files.

A tracked file is one that is in Git's index

The definition of tracked file is really simple, but subtle. A tracked file is any file that is currently in Git's index. The subtlety here lies in understanding Git's index. What is this thing? What is "the index"? Why does Git have three names for it, calling it the index, or—getting more common these days—the staging area, or—getting rarer and rarer now—the cache?

The index: background

The index, or staging area, is a piece of Git that Git hides from you for a while—there's no easy way to see it, not directly—but then, to anthropomorphize Git,1 Git randomly jumps out and slaps you in the face with its index, as if you were in this Monty Python sketch. So you need to know about it.

The index itself is very complicated, but the way you use it is pretty simple: you stage files in it. This is why it's now being more consistently called the staging area. But that still leaves the obvious question: What does it mean to stage a file? To answer that, we need to consider what a Git commit is.


1Don't anthropomorphize computers. They hate that.


Commits

A Git commit is:

  • Numbered: each commit has a unique hash ID, a big ugly random-looking number almost always expressed as a hexadecimal number. This hash ID is actually a cryptographic checksum2 of the complete contents of the commit, which means that the commit contents literally can't change: if you take a commit out, make some changes, and put the result back, what you get is a new commit with a new, unique hash ID; the old commit still exists, with its old hash ID.

  • Storage:

    • Each commit stores a full snapshot of all of your source files, in a special, read-only, Git-only format, compressed and de-duplicated.

    • Each commit also stores some metadata, or information about the commit itself. This includes stuff like who made it and when.

The hash-based numbering system, with a known cryptographic hash function, means that every Git in the universe agrees as to which hash ID any particular commit gets. This is how Git can be a distributed version control system. Two different Git repositories can tell if the other one has the same commit(s) just by checking hash IDs. That's not what we care about right here, but it's why commits are the way they are, so it's useful to know.


2The cryptography part is reminiscent of the blockchain of a cryptocurrency, though the cryptographic functions that Git uses today are too weak for this. Git only partly uses it for its Merkle tree properties; the fact that the hash has good entropy is, if anything, the most important aspect.


The working tree

In any case, the fact that the files inside commits are literally not usable by anything but Git means that to work on / with a commit, we have to have Git extract the commit. We have Git extract this commit to our working tree.

The working tree holds the files extracted from some commit. If we switch to a different commit, Git will remove these files, and extract, instead, the files from some other commit.

Okay, fine, so what? Well, suppose that our system—whatever it is—needs to "compile" or otherwise fiddle with files. We take our regular JavaScript or Typescript and turn these files into minified files, or we take our Python code and byte-compile it to *.pyc or *.pyo files, or we take our C or C++ code and compile it to *.o object files, or whatever.

These build artifacts also go into the working tree. But they shouldn't go into new commits. There are a lot of ways to handle this, but Git chooses a weird one.

The index, part 2: its role as staging area

The way Git handles this idea of which files do go into new commits, and which ones don't go into new commits, is to keep a separate copy of each to-be-committed file in its index aka staging area. These extra copies are in Git's internal form, compressed and de-duplicated, ready to be committed. Because they're de-duplicated, the files that came out of the commit literally take no space.3

The way Git handles this is that, when you first check out some commit, Git doesn't just fill in your working tree. Git fills in both your working tree and its index. So the index and your working tree now match.

As you do your work, you'll change some working tree files. As you do—and you choose when—Git requires that you run git add on these updated files. This step reads and compresses the working tree file. Git now checks to see if the compressed version is a duplicate, of any previous version of any file, and if so, simply re-uses the old file. If not, Git readies the contents for committing. Either way, Git now updates the index entry to record the new-or-reused contents.

What this means is that the index holds, at all times, your proposed next commit. Files that aren't in the index are not in your proposed next commit. Files that came out of the current commit, at git checkout or git switch time, are in the index, unless you have removed or replaced them: those files will be in the next commit, unless you have removed or replaced them, but they'll be the same content as the current commit (unless you've removed or replaced them) so they will take no space.

In this way, the index stays, at all times, your proposed next commit. It contains, "staged for commit", all the files.

The git status command will tell you about any staged-for-commit files that are different from the files in the current commit. It won't tell you about staged files that match the current commit, because that's not usually interesting. So if you have 3000 files ready to be committed, and two of them are different, git status tells you that you have two files staged for commit. You actually have 3000 files; it's just that two of them are different, leaving 2998 that are the same.

  • Exercise: If git status told you about all 3000 files, how would you find the interesting two?

Note that using git commit -a means, roughly,4:

  • run git add -u: this scans all the existing index (staged) files to see if any work-tree versions are newer, and if so, runs git add on them;
  • then run git commit.

This -a flag kind of lets you ignore the index for a while. It's a mistake to ignore it too much, though; I'll touch on that briefly in the next section. Note, too, that it won't ever add new files: for that, you have to do your own git add.


3Technically it's the file's content that takes no space. The index entry, with its cache data about the file as it appears in your working tree, takes some space, on the order of 40 to a few hundred bytes depending on many details, per file.

4The big difference between you running git add, and you having git commit run git add for you here, is what happens if your commit fails (due to a pre-commit hook, or you telling Git to stop and not make the commit after all, for instance). In this case the adding done by git commit -a gets un-done. If you run git add yourself, manually, the adding stays added.


Other uses for the index

The index plays a big role in conflicted merges. A Git merge involves not one but three commits, and to do the merge, Git reads all three commits into its index. This involves "expanding" the index, so that every file gets three slots. If the merge goes well, and Git is able to resolve every conflict on its own, the three slots get downgraded to the normal single-slot, ready-to-commit entry. If not, Git remembers that the conflict exists by leaving all three slots in place. We won't go into the details here, but the fact that the index expands during the merge means that you really can't ignore it.

Also, Git has git add -p: this lets you add part of a file, as it were. Git handles this by making a temporary copy of the file as seen in the index, in a regular file somewhere in the file system, then patching that file with an update, then adding that file's content to the index. The end result is that the copy that is in the index—proposed for the next commit—differs from both the current commit version and your working tree version. The only way to understand this is to recognize that the index holds the to-be-committed copy of the file, which may differ from any other copy.

Finally, the presence or absence of a file in the index is what determines whether a file is currently tracked or untracked. Since that affects whether the file can be ignored, you have to know about the index to understand .gitignore.

Summary of the index

Aside from its role in merges, the index, or staging area, simply holds the copy of each file that is going to be committed if you run git commit right now. You can change which files are in the index any time:

  • You can run git checkout or git switch to select a new commit, filling in both Git's index and your working tree.
  • You can run git rm to remove a file from both Git's index and your working tree.
  • You can run git rm --cached (there's the old, bad word cache, instead of staging area) to remove a file from Git's index, without touching the copy in your working tree.5
  • You can run git restore with the --staged option to copy a file into Git's index.
  • You can use various modes of git reset to affect index files (this command is large and perhaps too flexible, so we won't go into detail here).

Basically, there are lots of ways to change what is in Git's index, at any time. The index is not read-only, not like a commit is read-only. It contains the proposed next commit and when you run git commit, that packages up whatever files are in the index right then and freezes them forever, into a new commit.6 This brings us back to tracked vs untracked files: A tracked file is one that is in Git's index right now. An untracked file is one that is not in Git's index right now. And, you can only ignore untracked files.


5Note that this means the next commit won't have the file. This sets up a bad situation if you ever check out the old commit, that does have the file: now the working tree copy can get removed when you switch back to the new commit, that doesn't have the file.

6The new commit only gets made if it gets made, of course—see the earlier remark about pre-commit hooks for instance—and, while the new commit permanently reserves its new hash ID for the new frozen set of files and metadata, you can, with some work, make Git forget that this commit exists. You can't change the commit, once you make it, but you can stop using it. The new commit, with its hash ID, is read-only forever, but it only lasts as long as it lasts. Commits are read-only, but only semi-permanent.


Exclusion files like .gitignore

A file that is in your working tree, but isn't in Git's index, is an untracked file. We could just stop here, because that's sufficient. You run git commit and only the tracked files get committed. You just very carefully git add only the right files, and only those files, plus the ones from the earlier commit, get committed.

But this method of working is annoying and inefficient. We run git status and, whoa hey, Git whines about 3000 untracked files. Where is our good information? It's buried under all this "untracked files" nonsense.

(Did you do that earlier exercise, about finding the good information when it is buried in a ton of useless information?)

So: what if we can get Git to shut the bleep up about most of these untracked files? That could be the most important thing we could do next, when we're using Git. That makes git status only print interesting things.

And of course, we can get Git to shut up. We just need to list the files that it shouldn't tell us about. We can put these files' names in .gitignore, or .git/info/excludes, or ~/.config/git/ignore, or some other file. Collectively, these files are exclusion files. Colloquially, most people just call them .gitignore.

A .gitignore file can contain:

  • a glob pattern, like *.pyc
  • a simple file name, like debug
  • a path name with a leading slash, like /tags
  • a path name with an embedded slash, like contrib/buildsystems/out

and more.

When one of these files lists a simple file name or pattern, like debug or *.pyc, this name matches any file or directory name in the same directory that the .gitignore file is found in, or in any directory underneath that point. So if we have a project with, as you say:

.gitignore
LICENSE
README.md
App/
    .gitignore
    main.py
    [more files and directories]

then we have both /.gitignore, the one at the top level, and /App/.gitignore, the one in the App sub-directory. The otherwise unnecessary, and sometimes misleading, leading / here—/.gitignore—is there just to tell us at which level we found the file.

Simple entries in the /.gitignore apply here and to stuff in App/, and to stuff in App/sub/ if it exists, and so on. But an entry in the /.gitignore file that has a leading slash in it, like /README.html for an HTML-ized version of the markdown file that we might have some software build for us, won't match a README.html in App, if there is one.

So, the leading slash in an entry in a .gitignore file has the effect of anchoring the entry: it matches only at this particular level. If /App/.gitignore contains /LICENSE, this will ignore /App/LICENSE but not the /LICENSE in the top level.

Anchoring, in a .gitignore file, is a little weird, because any embedded slash will also have this anchoring effect. Hence if you want to ignore contrib/buildsystems/out, as the Git repository for Git does, you can write:

contrib/buildsystems/out

or:

/contrib/buildsystems/out

in the top level .gitignore file: both mean the same thing. The Git folks chose to use the latter, perhaps on the theory that it's clearer (which I think it is, slightly).

There are more patterns and rules we can use in .gitignore files, but this is probably enough for now. The details are described in the gitignore documentation. What we really need to talk about here is the other effects of a .gitignore entry.

We already know that listing some pattern, with respect to some untracked file, will cause git status to shut up about it. This is important: it makes git status usable. But it has two more effects:

  • It makes git add easier to use.
  • It has an unfortunate "permission to clobber" side effect.

Let's address the first one now.

En-masse git add

To make git add easy to use, we can list, in the .gitignore file, those files that should not be added if they're not already tracked. If the .gitignore file includes *.pyc and *.pyo and __pycache__, for instance, then whether we're using Python 2 or Python 3, we can just run:

git add .

and not worry about our byte-code-compiled Python files getting into a commit. That's because the en-masse git add ., which scans through the current directory, will not add as a tracked file any file that is both untracked and ignored.

Note that if a file is already tracked, git add . will add it. That is, if some file that matches a .gitignore pattern is already in Git's index, and you use a git add command that would add it if it weren't listed as ignored, Git acts as if it's not listed as ignored. The fact that it's in the index right now overrides everything else: it isn't ignored, so it won't be ignored.

You can also use git add -f to forcibly override an ignore rule. See How to use .gitignore to ignore everything in a directory except one file? for example. This lets you get an otherwise-ignored file into the index "the first time", after which its existence in the index ensures its continued existence (and updating) in the index. I personally don't like this technique, since it means if you ever run git rm on the file, a later git add won't notice that it should be added back: once it is out of the index, it stays out!

Permission to clobber

This one is tricky. It does not occur very often, either. The main way to trigger it is to have a file that's tracked and committed, and then decide that you don't want it tracked, but do want to keep it around as an untracked file in the typical working tree. You therefore use git rm --cached to remove it from Git's index, and commit, and then carefully update all users of clones of this repository.

But then, some day, you decide to check out some old commit, to look at how things were. This overwrites your working tree copy of the file. Git needs a way to say that some file should not be committed, but also should not be clobbered. It doesn't have one. For further details, see Git checkout has deleted untracked files unintentionally.

Conclusion

If you made it this far, congratulations! You now know a great deal more than most casual Git users ever learn about the interactions between Git's index and .gitignore files, and what can go into those exclusion files.

The ultimate answer is that you should do what works for you. Git is, after all, a set of tools, not a solution. There's no single right answer to any one given problem (although some simple problems do have one simple answer, which is therefore probably the one to use). If you have an unusual situation, you may need an unusual solution. Git may be able to provide it.

Remember how exclusion file entries are matched (see my somewhat detailed description in How to use .gitignore to ignore everything in a directory except one file?). Use git check-ignore -v as needed to see if some file name is being matched, and if so, by which rule. Remember that your personal exclusion file, in ~/.gitignore or ~/.config/git/ignore, can be used to keep Git from en-masse adding editor temporaries like *.swp or *.swo (vim), *.~ (emacs backups), and the like. A per-repository exclusion file in .git/info/exclude can hide files that you're creating in this particular repository using some interactive debugger, if you don't want to list them in your personal exclusion file or a local .gitignore that gets committed into this repository.

You have a lot of tools. Some of them have sharp edges (like the potential to clobber files), so use them, but use them carefully.

torek
  • 448,244
  • 59
  • 642
  • 775