Gitignore-A-Palooza: Two Tricks

Posted on Sun 02 December 2018 in Software Development

Most modern software developers are aware of Git and its accompanying gitignore file. This post offers two lesser-known-yet-useful techniques to ignore files from a Git-based version controlled project. The first technique enables Git to ignore every file in a directory but the directory itself. The second technique enables a user to automatically ignore a file without adding it to a project's root gitignore.

Note: when discussing "gitignore" and "tern-project", I may sometimes add a "." before the filename. Whether I do or do not, I'm talking about the same file (eg, ".gitignore" == "gitignore").

1: Ignore directory contents, keep directory

It is often convenient to have a directory whose contents are not committed to version control. For example, this directory may contain large non-general data files (images, financial data, etc). Additionally, these could be small files with sensitive information specific to one person (passwords, ssh keys, etc). Either way, we do not want these files committed to our project's version control system. For the purposes of this conversation, we'll call this folder "instance" and place it at the root of our project.

├── app
│   └── __init__.py
├── .gitignore          <-- Project gitignore
└── instance
    ├── big-file.jpg
    └── passwords.txt

A naive approach suggests that we add the instance folder to our project's gitignore:

*.pyc
__pycache__/
# other ignored lines
instance/

Unfortunately, this proves problematic. What happens if our system relies on a file being in the instance folder at runtime? A user of our system would need to know to create the instance folder AND place a file there, forcing them to perform two manual steps.

A preferable solution: place a gitignore in the "instance" folder with special contents.

├── app
│   └── __init__.py
├── .gitignore          <-- Project gitignore
└── instance
    ├── big-file.jpg
    ├── .gitignore      <-- Special gitignore
    └── passwords.txt
-----------------------------------
special .gitignore contents:
# Ignore all files in this directory
# EXCEPT for this .gitignore file
*
!.gitignore

I find myself using this trick so often that I've added the instance folder and its accompanying gitignore file into my project skeletons.

# ~/.bashrc (I actually use zsh + ~/.zshrc, but it's the same)
function make_instance() {
  mkdir instance
  cat > instance/.gitignore <<EOL
# Ignore all files in this directory
# EXCEPT for this .gitignore file
*
!.gitignore
EOL
}

Finally, you may note that this technique does commit one file (the gitignore) to version control. IMHO, this is a small price to pay for elegance.

2: Ignore my weird files (in secret)

My development environment is highly customized. I'm an advanced user of Neovim, Tmux, Zsh, and other Unix-like utilities, and have set up a cross-language, bespoke IDE with a configuration unto itself. Benefits include software development bliss. One downside: my environment may require files that I should not commit to another person's repository.

One example: when programming in Javascript, I rely on a Tern server for auto-completion in Neovim. Tern is configurable with a tern-project file which lets Tern know important things like the project's ECMAScript version, which runtime environment it's expecting, etc. These configurations differ by project, so it's often necessary for me to place a customized tern-project file at the root of a Javascript project.

If I own a Javascript codebase and control its development, placing a tern-project file at my project's root is a no-brainer. Not only can collaborators help me keep it up to date, but I can also influence more people to adopt Tern! Unfortunately, if I don't own the codebase I'm working on, things become a bit trickier. I want to benefit from Tern but I don't want my first contributions on a project to include my custom development environment configuration. I'd simply add this file to the project's .gitignore file, but that would still be an me-specific contribution, so I'm left with two choices. Either I manually ignore tern-project every time I "git add/commit", or I find a way to make Git do that work for me.

Luckily, Git has a built-in way for users to ignore their own weird files without needing to make any updates to a project's version-controlled files. This method is mentioned in gitignore's documentation, but not many people are aware it exists.

Using your favorite editor, go to the root of any Git repository, and open the text file ".git/info/exclude". It should look like this:

# Contents of PROJECT-ROOT/.git/info/exclude:
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

Simply add your desired file patterns here and they will be ignored by you, and only you.

# Contents of PROJECT-ROOT/.git/info/exclude:
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.tern-project

Once you've done this, I recommend submitting one or two impressive pull requests. You should be invited as a repository collaborator (or its equivalent) in no time. With credentials in hand, you can then suggest the group adopt your awesome Tern-based workflow (and your humble tern-project file) into the project. At this point, you should remove the ".tern-project" line from .git/info/exclude.

Conclusion

Ignoring directory contents while retaining directories is easy. Privately ignoring the files only you care about (for now) is also easy. Hopefully you learned something from this post and happy hacking!