Chapter 15: Ignoring Files in Git to Keep them Out

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the purpose and importance of ignoring files in a Git repository.
  • Create and use .gitignore files to specify patterns for files and directories that Git should ignore.
  • Master the syntax and various pattern rules for .gitignore files.
  • Set up and use a global .gitignore file for user-specific ignore patterns.
  • Learn how to make Git ignore files that were previously tracked.
  • Debug .gitignore rules using git check-ignore.
  • Understand and apply the .gitkeep convention for tracking empty directories.

Introduction

When working on any software project, you’ll inevitably generate files that you don’t want to include in your Git repository’s history. These might be build artifacts, log files, temporary files created by your editor or IDE, local configuration overrides, or sensitive information like API keys (though .gitignore is not a foolproof security measure for already committed data). Including such files in your repository can bloat its size, create unnecessary merge conflicts, and clutter your git status output, making it harder to see meaningful changes.

Git provides a simple yet powerful mechanism to specify intentionally untracked files that Git should ignore: the .gitignore file. By defining patterns in .gitignore files, you can tell Git to disregard certain files and directories, keeping your repository clean, focused on source code and essential project assets, and easier for collaborators to work with.

This chapter will guide you through the process of creating effective .gitignore files, understanding their syntax, managing global ignore rules, and handling scenarios where you need to ignore files that were previously tracked. We’ll also cover how to debug your ignore rules and a common convention for tracking otherwise empty directories.

Theory

What is .gitignore?

A .gitignore file is a plain text file where each line contains a pattern for files or directories that Git should ignore. Git checks these patterns when determining which files are “untracked.” If an untracked file matches a pattern in a relevant .gitignore file, Git will not list it in git status (unless explicitly asked to show ignored files) and will not include it in git add . operations.

Why Ignore Files?

Common types of files you’ll want to ignore include:

  • Compiled code/Build artifacts: .class files, .o files, executables, bin/, build/, dist/ directories.
  • Log files: .log files, application logs.
  • Runtime files: PID files, temporary outputs.
  • Dependency directories: node_modules/, vendor/ (though this depends on the project’s dependency management strategy; some prefer to commit dependencies).
  • Editor/IDE configuration files: .idea/ (IntelliJ), .vscode/ (VS Code, though some parts like launch.json might be shared), .project (Eclipse), editor backup files like *~ or *.swp.
  • OS-generated files: .DS_Store (macOS), Thumbs.db (Windows).
  • Sensitive information: Files containing API keys, passwords, or other credentials (e.g., credentials.json, config.local.php). Important: .gitignore only prevents newly untracked files from being added. If sensitive data has already been committed, it’s in the history and requires more advanced techniques (like git filter-repo from Chapter 13) to remove.

.gitignore File Location and Scope

  • Repository Root: The most common place for a .gitignore file is in the root directory of your Git repository. Patterns in this file apply to the entire repository.
  • Subdirectories: You can also place .gitignore files in subdirectories. Patterns in a subdirectory’s .gitignore file are relative to that directory and apply to that directory and its subdirectories. These rules are evaluated in addition to rules from parent directories.
  • Precedence: Rules in a .gitignore file in a subdirectory take precedence over rules in a parent directory for paths within that subdirectory.

.gitignore Syntax and Patterns

The rules for patterns in .gitignore files are as follows:

Pattern Element / Rule Description Example(s)
Blank Lines Ignored; can be used for readability. (Just an empty line)
# (Hash) Lines starting with # are comments and are ignored. # This is a comment
Trailing Spaces Ignored unless quoted with a backslash (\ ). file_with_space\
* (Asterisk) Matches zero or more characters, but not directory separators (/). *.log (matches debug.log, app.log)
temp* (matches tempfile, temporary)
? (Question Mark) Matches any single character, except /. file?.txt (matches file1.txt, fileA.txt)
[set] or [range] Matches any one character from the set (e.g., [abc]) or range (e.g., [0-9]). image[1-3].png (matches image1.png, image2.png, image3.png)
/ (Forward Slash) – Beginning Anchors the pattern to the directory where the .gitignore file resides. Only matches items in that specific directory. /TODO (matches ./TODO, not src/TODO)
/ (Forward Slash) – Middle Treated as a directory separator. logs/debug.log (matches debug.log inside any logs directory relative to the .gitignore file, or ./logs/debug.log if not starting with **)
/ (Forward Slash) – End Pattern only matches directories. Everything inside the directory will also be ignored. build/ (ignores the build directory and its contents)
** (Double Asterisk) Matches zero or more directories (i.e., matches across directory levels).
**/foo: matches foo file or directory in any subdirectory.
a/**/b: matches b inside a at any depth.
a/**: matches everything inside directory a.
**/logs (matches logs, src/logs, a/b/logs)
*.tmp/** (matches any .tmp directory and everything within it)
! (Exclamation Mark) Negates the pattern. Re-includes a file if it was excluded by a previous pattern. Precedence matters: an earlier exclusion is necessary for a negation to have an effect. A later exclusion can override an earlier inclusion. *.log
!important.log (ignore all .log files except important.log)
  1. Blank Lines: Blank lines are ignored and can be used for readability.
  2. Comments: Lines starting with a hash (#) are treated as comments and are ignored.
  3. Trailing Spaces: Trailing spaces are ignored unless they are quoted with a backslash (\ ).
  4. Glob Patterns: Git uses glob patterns, similar to those used in shell environments, to match file and directory names.
    • *: Matches zero or more characters, but does not match directory separators (/). For example, *.log matches debug.log and error.log but not logs/debug.log.
    • ?: Matches any single character, except /. For example, file?.txt matches file1.txt and fileA.txt.
    • [abc]: Matches any one character from the set a, b, or c.
    • [0-9]: Matches any digit from 0 to 9.
  5. Directory Separator (/):
    • If a pattern starts with a /, it anchors the pattern to the directory where the .gitignore file resides. For example, /TODO in the root .gitignore matches TODO in the root, but not src/TODO.
    • If a pattern contains a / elsewhere, it’s treated as a directory separator. For example, logs/debug.log matches debug.log inside a logs directory.
    • If a pattern ends with a /, it only matches directories. For example, build/ matches the build directory and everything inside it, but not a file named build.
  6. Double Asterisk (**):
    • **/logs: Matches logs directories anywhere (e.g., logs/, src/logs/).
    • foo/**/bar: Matches bar files or directories inside foo at any depth (e.g., foo/bar, foo/a/bar, foo/a/b/bar).
    • abc/**: Matches everything inside the abc directory.
  7. Negation (!):
    • An optional prefix ! negates the pattern. Any matching file excluded by a previous pattern will become included again if it also matches a negated pattern.
    • Precedence is important: For a ! pattern to re-include a file, the file must first be excluded by a non-! pattern defined earlier in the .gitignore files or by a pattern in a .gitignore file in a parent directory. A later exclusion rule can override an earlier inclusion rule.
    • Example:*.log # Ignore all .log files !important.log # But do track important.log
      Another example:logs/ # Ignore the logs directory !logs/.gitkeep # But track the .gitkeep file in it
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph LR
    subgraph "Project Directory Structure"
        direction LR
        P["ProjectRoot/"]
        P --> F1[".gitignore"]
        P --> F2["app.py"]
        P --> D1["build/"]
        D1 --> DF1["output.exe"]
        D1 --> DF2["temp.o"]
        P --> D2["logs/"]
        D2 --> LF1["debug.log"]
        D2 --> LF2["trace.log"]
        P --> D3["src/"]
        D3 --> SF1["main.c"]
        D3 --> SD1["vendor/"]
        SD1 --> SVF1["library.lib"]
        P --> F3["config.ini"]
        P --> F4["IMPORTANT.md"]
        P --> F5[".DS_Store"]

        class F1,F2,F3,F4,F5,DF1,DF2,LF1,LF2,SF1,SVF1 fileNode;
        class D1,D2,D3,SD1 dirNode;
    end

    subgraph ".gitignore Contents & Effects"
        direction TB
        R1["Pattern: <b>build/</b>"] --> E1["Effect: Ignores <i>build/</i> directory and all its contents (<i>output.exe</i>, <i>temp.o</i>)"]
        R2["Pattern: <b>*.log</b>"] --> E2["Effect: Ignores <i>debug.log</i> and <i>trace.log</i>"]
        R3["Pattern: <b>src/vendor/</b>"] --> E3["Effect: Ignores <i>src/vendor/</i> and its contents (<i>library.lib</i>)"]
        R4["Pattern: <b>**/*.o</b>"] --> E4["Effect: Ignores <i>temp.o</i> in <i>build/</i> (and any <i>.o</i> file in any subdirectory)"]
        R5["Pattern: <b>.DS_Store</b>"] --> E5["Effect: Ignores <i>.DS_Store</i> file"]
        R6["Pattern: <b>!IMPORTANT.md</b><br>(Assuming <i>*.md</i> was ignored earlier or by a broader rule)"] --> E6["Effect: Re-includes <i>IMPORTANT.md</i> even if other <i>*.md</i> files are ignored"]
        R7["Pattern: <b>config.ini</b>"] --> E7["Effect: Ignores <i>config.ini</i> specifically"]

        class R1,R2,R3,R4,R5,R6,R7 patternNode;
        class E1,E2,E3,E4,E5,E6,E7 effectNode;
    end

    %% Styling
    classDef fileNode fill:#DBEAFE,stroke:#2563EB,color:#1E40AF;
    classDef dirNode fill:#E0E7FF,stroke:#4338CA,color:#3730A3,font-weight:bold;
    classDef patternNode fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6,font-size:12px;
    classDef effectNode fill:#F3F4F6,stroke:#9CA3AF,color:#374151,font-size:11px,padding:5px;

    %% Linking examples to file structure (conceptual)
    E1 -.-> D1;
    E2 -.-> LF1;
    E2 -.-> LF2;
    E3 -.-> SD1;
    E4 -.-> DF2;
    E5 -.-> F5;
    E6 -.-> F4;
    E7 -.-> F3;

    style P fill:#FFFBEB,stroke:#FBBF24,font-weight:bold;

Global .gitignore

Sometimes you have ignore patterns that apply to all your repositories on your machine, such as files generated by your personal editor setup or OS-specific files (e.g., .DS_Store on macOS, Thumbs.db on Windows). Instead of adding these to every project’s .gitignore file, you can use a global .gitignore file.

  1. Create a global ignore file (e.g., ~/.gitignore_global or ~/.config/git/ignore). The name and location are up to you.
  2. Tell Git where to find this file using the core.excludesFile configuration option:git config --global core.excludesFile ~/.gitignore_global
    (Replace ~/.gitignore_global with the actual path to your global ignore file.)

Patterns in this global file apply to all repositories for your user account.

Category / Operating System Common Files / Directories to Ignore Globally Notes
macOS .DS_Store
._*
.Spotlight-V100
.Trashes
__MACOSX
Finder metadata, resource forks, Spotlight index, trash items, archive metadata.
Windows Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
Thumbnail cache, desktop configuration, recycle bin.
Linux (General) *~ (common editor backup suffix)
*.swp, *.swo (Vim swap files)
Backup files from editors like Emacs, Vim, Gedit.
Common IDEs / Editors .idea/ (IntelliJ IDEA, WebStorm, etc. – some files within might be shared, like runConfigurations)
.vscode/ (VS Code – user-specific settings like tasks.json, workspaceStorage; some like launch.json might be shared)
*.sublime-workspace, *.sublime-project (Sublime Text)
.project, .classpath, .settings/ (Eclipse)
nbproject/ (NetBeans)
User-specific workspace, project settings, history, or cache files. Check IDE documentation for recommended per-user ignores.
Personal Build/Tool Outputs *.bak
*.tmp
local_env_setup.*
Generic backup files, temporary files, or personal scripts not meant for sharing.

Ignoring Previously Tracked Files

It’s crucial to understand that .gitignore files only prevent untracked files from being added to Git’s tracking (the index/staging area). If a file is already tracked by Git (i.e., it has been committed), adding a pattern for it to .gitignore will not make Git ignore it. git status will still show modifications to that file.

To stop tracking a file that is already in the repository and then ignore it:

  1. Remove the file from Git’s tracking (the index/staging area), but keep it in your working directory:git rm --cached <file_or_directory>
    For a directory, use the -r (recursive) option:git rm -r --cached <directory_name>
    The --cached option is key here; it removes the file from the index, but leaves your local copy intact. Without --cached, git rm would also delete the file from your working directory.
  2. Add the file’s pattern to your .gitignore file.For example, if you removed config.local.ini, add config.local.ini or a more general pattern like *.local.ini to .gitignore.
  3. Commit the changes:This will involve committing the removal of the file from tracking (recorded as a deletion in the index) and committing the updated .gitignore file.git add .gitignore git commit -m "Stop tracking <file_or_directory> and add to .gitignore"

Warning: This procedure removes the file from being tracked going forward. The file will still exist in the repository’s history in commits made before this change. To completely remove a file from all history (e.g., if sensitive data was committed), you need to use more advanced history rewriting tools like git filter-repo (as discussed in Chapter 13), which is a destructive operation and should be done with extreme caution, especially on shared repositories.

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    A["Start: File *config.log* is currently tracked by Git (committed)."] --> B{"Goal: Stop tracking *config.log* and ignore future changes."};
    B --> C["<b>Step 1:</b> Remove file from Git's tracking (index/staging area)<br>but keep it in the working directory."];
    C --> D["Command: *git rm --cached config.log*"];
    D --> E["Result: *config.log* is no longer staged for commit (Git sees it as 'deleted' from index).<br>The actual file *config.log* remains in your local working directory."];
    E --> F["<b>Step 2:</b> Add the file (or a pattern matching it) to *.gitignore*."];
    F --> G["Edit *.gitignore* file, add line:<br>*config.log* (or **.log* if appropriate)"];
    G --> H["<b>Step 3:</b> Stage and commit these changes."];
    H --> I["Command: *git add .gitignore* (and *git add config.log* if it appears as deleted in status, due to *rm --cached*)<br>Alternatively, *git add .gitignore config.log* or *git add .*"];
    I --> J["Command: *git commit -m \Stop tracking config.log and add to .gitignore\*"];
    J --> K["<b>Outcome:</b><br>- *config.log* is no longer tracked by Git.<br>- Future changes to *config.log* will be ignored.<br>- The file still exists in your local working directory.<br>- *Important:* The file's content from previous commits remains in the repository history."];
    K --> Z["End: File is untracked and ignored going forward."];
    subgraph "Important Note on History"
        Note1["*.gitignore* only prevents future tracking of untracked files."]
        Note2["To remove a file from <b>all</b> past history (e.g., sensitive data),<br>more advanced tools like *git filter-repo* are needed (see Chapter 13).<br>This is a destructive operation and should be handled with extreme care."]
        class Note1,Note2 warningNote;
    end
    K -.-> Note1;
    %% Styling
    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef commandNode fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3,font-family:monospace;
    classDef resultNode fill:#F3F4F6,stroke:#9CA3AF,color:#374151,font-size:11px;
    classDef outcomeNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef warningNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E,font-style:italic;
    classDef decisionNode fill:#FEF9C3,stroke:#F59E0B,stroke-width:1px,color:#78350F;
    class A startNode;
    class B decisionNode;
    class C,F,H processNode;
    class D,G,I,J commandNode;
    class E,K resultNode;
    class Z outcomeNode;
    class Note1,Note2 warningNode;

Debugging .gitignore

Sometimes, it’s not clear why a file is being ignored or, conversely, why it’s not being ignored when you expect it to be. Git provides a command to help debug this: git check-ignore.

  • git check-ignore <pathspec>…:This command checks the given path(s) against your .gitignore rules (from all applicable .gitignore files and the global excludes file). If a path is ignored, the command will output the path. If not, it outputs nothing.git check-ignore myapp.log # Output if ignored: myapp.log
  • git check-ignore -v <pathspec>… or git check-ignore –verbose <pathspec>…:This is the most useful mode for debugging. For each path that is ignored, it outputs three pieces of information, colon-separated:
    1. The path to the .gitignore file containing the matching pattern.
    2. The line number of the matching pattern within that file.
    3. The pattern itself.
    git check-ignore -v myapp.log node_modules/somepackage/file.js # Example output: # .gitignore:1:*.log myapp.log # .gitignore:5:node_modules/ node_modules/somepackage/file.js
    If a path is not ignored, there will be no output for that path.
  • git check-ignore -n <pathspec>… or git check-ignore –non-matching <pathspec>…:This option, often used with -v, will also print details for paths that are not ignored. For non-matching paths, the second and third fields (line number and pattern) will be empty.
  • git status –ignored:This command shows untracked files, and if you provide the –ignored flag, it will also list ignored files that are currently present in your working directory. This can be a quick way to see what Git considers ignored.

The .gitkeep Convention

Git does not track empty directories. If you git add an empty directory, nothing happens. However, sometimes you want an empty directory structure to be present in the repository (e.g., a logs/ directory, tmp/, or an empty src/main/resources in a Java project before resources are added).

To work around this, developers often use a convention: placing an empty file (or a file with minimal content) named .gitkeep inside an otherwise empty directory. This makes the directory non-empty, allowing Git to track the .gitkeep file, and thus, the directory structure.

  • .gitkeep is not an official Git feature or command. It’s purely a community convention. The name .gitkeep is just a common choice; you could name it .keep, placeholder.txt, etc.
  • If you have a rule in .gitignore that would ignore all files in a directory (e.g., logs/*), but you want to keep the logs/ directory structure using .gitkeep, you would need to explicitly un-ignore .gitkeep:# Ignore all files in logs directory logs/* # But do not ignore .gitkeep files within logs !logs/.gitkeep # Or more generally, if you use .gitkeep in many places: # !.gitkeep

Practical Examples

Setup:

Create a new Git repository for these examples.

Bash
mkdir gitignore-lab
cd gitignore-lab
git init

# Create some files and directories
touch app.py
mkdir build
touch build/output.exe
mkdir logs
touch logs/debug.log
touch logs/trace.log
mkdir data
touch data/user_data.csv
touch .DS_Store # Simulate macOS file
touch Thumbs.db # Simulate Windows file
mkdir src
touch src/main.c
mkdir src/editor_config
touch src/editor_config/.vscode_settings
touch my_notes.txt~ # Simulate editor backup file

Current git status would show all these as untracked files.

1. Creating a Basic .gitignore File

Create a file named .gitignore in the root of gitignore-lab with the following content:

Bash
# .gitignore

# Ignore build artifacts
build/
*.exe

# Ignore log files
logs/
*.log

# Ignore OS-specific files
.DS_Store
Thumbs.db

# Ignore editor backup files
*~

# Ignore specific sensitive files (example)
# credentials.secret

Now, check the status:

Bash
git status

Expected Output (files like app.py, data/user_data.csv, src/main.c, src/editor_config/.vscode_settings should still be listed as untracked, but the others should be gone from the list):

Bash
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitignore
        app.py
        data/
        src/

nothing added to commit but untracked files present (use "git add" to track)

The build/, logs/, .DS_Store, Thumbs.db, and my_notes.txt~ are no longer listed as untracked because they match patterns in .gitignore.

Tip: Always commit your .gitignore file to the repository so that these ignore rules are shared with all collaborators.

Bash
git add .gitignore
git commit -m "Add initial .gitignore file"

2. Ignoring Specific Files and Using Negation

Let’s say we want to ignore all .csv files in the data/ directory, but specifically track data/important_report.csv.

Modify .gitignore:

Bash
# ... (previous rules) ...

# Ignore CSV files in data directory
data/*.csv

# But track important_report.csv
!data/important_report.csv

Now create these files:

Bash
touch data/temp_data.csv
touch data/important_report.csv

Check status:

Bash
git status

Expected Output:

Bash
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        app.py
        data/important_report.csv  # This one is NOT ignored
        src/

nothing added to commit but untracked files present (use "git add" to track)

data/temp_data.csv` is ignored, but `data/important_report.csv` is still listed as untracked (meaning it’s not ignored).

3. Setting up a Global .gitignore

Let’s say you always use an editor that creates *.bak backup files, and you want to ignore these for all your projects.

  1. Create a global ignore file, e.g., ~/.gitignore_global: # ~/.gitignore_global *.bak .my_personal_editor_prefs
  2. Configure Git to use it:
    bash git config --global core.excludesFile ~/.gitignore_global
    (Adjust path if you named or placed your global file differently.)

Now, go back to your gitignore-lab project (or any other Git project) and create a .bak file:

Bash
# In gitignore-lab directory
touch my_document.txt.bak
git status

my_document.txt.bak should not appear in the git status` output because it’s ignored by your global configuration.

4. Ignoring an Already Tracked File

Suppose app.py was accidentally committed, but now we realize it’s a local script and shouldn’t be tracked.

First, let’s commit it to simulate the scenario:

Bash
git add app.py
git commit -m "Add app.py script"
# Now app.py is tracked

Now app.py is trackedNow, we want to stop tracking it and ignore it:

  1. Remove app.py from Git’s tracking (but keep the local file):git rm --cached app.py
    Output:rm 'app.py'
  2. Add app.py to .gitignore:# ... (previous rules) ... app.py
  3. Commit these changes:git add .gitignore git commit -m "Stop tracking app.py and add to .gitignore"

Now, git status will no longer show changes to app.py, and it’s effectively ignored for future commits. It still exists in your working directory.

5. Debugging with git check-ignore -v

Let’s check why some files are ignored:

Bash
git check-ignore -v logs/debug.log build/output.exe data/important_report.csv src/main.c

Expected Output (paths and line numbers might vary slightly based on your exact .gitignore):

Bash
.gitignore:5:logs/	logs/debug.log
.gitignore:2:build/	build/output.exe
# No output for data/important_report.csv because it's NOT ignored (due to the '!' rule)
# No output for src/main.c because it's NOT ignored

This output tells you exactly which pattern in which .gitignore file is causing a file to be ignored. If data/important_report.csv was checked, it wouldn’t appear in the output because it’s not ignored.

6. Using .gitkeep

Let’s say we want an empty directory tmp/ in our project.

Bash
mkdir tmp
git status # 'tmp/' won't show up as Git doesn't track empty dirs

Add a .gitkeep file:

Bash
touch tmp/.gitkeep
git status

Expected Output:

Bash
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/
        tmp/

nothing added to commit but untracked files present (use "git add" to track)

Now tmp/ (because it contains .gitkeep) is listed. You can git add tmp/.gitkeep to track the directory structure.

If you wanted to ignore everything else in tmp/:

Add to .gitignore:

Bash
# ...
tmp/*
!tmp/.gitkeep

OS-Specific Notes

  • Path Separators: Always use forward slashes (/) as path separators in .gitignore files. Git internally converts them to be platform-independent, so / works correctly on Windows, macOS, and Linux.
  • Case Sensitivity:
    • On case-sensitive file systems (like those typical on Linux), patterns in .gitignore are case-sensitive by default. MyFile.log is different from myfile.log.
    • On case-insensitive file systems (default for Windows and macOS), Git’s behavior regarding .gitignore patterns can be influenced by the core.ignorecase setting (which defaults to true on these systems). Generally, patterns will match case-insensitively. For example, *.log would ignore DEBUG.LOG and debug.log.
    • To ensure consistent behavior across platforms, it’s often best to write patterns in lowercase and ensure filenames also follow a consistent case if cross-platform collaboration is expected.
  • Common OS-Specific Files to Ignore Globally:
    • macOS: .DS_Store, ._*, .Spotlight-V100, .Trashes
    • Windows: Thumbs.db, ehthumbs.db, Desktop.ini, $RECYCLE.BIN/
    • Linux (editors/misc): *~ (Emacs/Vim backups), *.swp (Vim swap files)These are excellent candidates for your global .gitignore file (core.excludesFile). Many template .gitignore files for specific languages or frameworks available online (e.g., from github.com/github/gitignore) include common OS-specific ignores.

Common Mistakes & Troubleshooting Tips

Issue / Symptom Common Cause(s) Solution / Tip
File is already tracked and .gitignore doesn’t ignore it. .gitignore only prevents untracked files from being added. It does not affect files already in Git’s index. 1. Remove the file from Git’s tracking (but keep it locally): git rm --cached <file> (use -r for directories).
2. Add/verify the pattern for the file in .gitignore.
3. Commit both changes: git add .gitignore <file_if_rm_was_staged> (or git add . if appropriate), then git commit -m "Stop tracking <file>".
Pattern in .gitignore not working as expected. Incorrect pattern syntax (e.g., wrong wildcards, path issues, misunderstanding of / or **). Typos. Case sensitivity issues on some systems. Review .gitignore syntax rules carefully.
Use git check-ignore -v <path_to_file> to see which pattern (if any) from which .gitignore file is matching the file.
Test patterns incrementally. Ensure correct use of forward slashes / for paths.
Global .gitignore not taking effect. Path to global ignore file in core.excludesFile is incorrect or file doesn’t exist at that path. Local .gitignore rules (especially ! patterns) might be overriding global rules. Verify the path with git config --global core.excludesFile.
Ensure the global ignore file exists and has correct patterns.
Use git check-ignore -v <path_to_file> to see if a global rule is being applied or if a local rule is taking precedence.
Forgetting to commit .gitignore file(s). Ignore rules work locally for the developer who made the change, but not for collaborators or in CI/CD environments after they pull. Always git add .gitignore and git commit any changes to .gitignore files so the rules are versioned and shared with the project.
Negation pattern (!) not working. A negation pattern only re-includes a file if it was excluded by a previous pattern in the same file or a .gitignore file in a parent directory. A later exclusion rule can override an earlier inclusion. The order of rules matters. Ensure an exclusion pattern for the file (or a broader pattern covering it) appears before the negation pattern.
Example:
*.log
!important.log (Correct)

!important.log
*.log (Incorrect – important.log will be ignored by the later *.log rule)
Trying to ignore changes to a tracked file. A file is tracked, but you want Git to temporarily ignore local modifications to it without removing it from the repository. .gitignore is not for this. Use git update-index --assume-unchanged <file> to tell Git to temporarily ignore changes to this specific tracked file (local setting).
To start tracking changes again: git update-index --no-assume-unchanged <file>.
Use with caution as it can lead to forgetting to commit important changes.

Exercises

  1. Comprehensive Project .gitignore:
    1. Create a new project directory and initialize a Git repository.
    2. Imagine this is a Python project. Create the following files/directories:
      • main.py
      • tests/test_utils.py
      • venv/ (a virtual environment directory)
      • __pycache__/some_module.cpython-39.pyc
      • docs/_build/html/index.html
      • output.log
      • .env (for local environment variables)
      • secrets.ini (containing mock sensitive data)
    3. Create a .gitignore file in the project root to:
      • Ignore the venv/ directory.
      • Ignore all __pycache__/ directories.
      • Ignore all .pyc files.
      • Ignore the docs/_build/ directory.
      • Ignore all .log files.
      • Ignore the .env file.
      • Ignore secrets.ini.
    4. Run git status to verify that only main.py, tests/test_utils.py, and .gitignore itself are listed as untracked.
    5. Use git check-ignore -v on several of the ignored files to see which rule is causing them to be ignored.
  2. Selective Ignoring and .gitkeep:
    1. In the same project, create a directory temp_outputs/.
    2. You want to keep the temp_outputs/ directory in the repository structure but ignore all its contents, except for a .gitkeep file.
    3. Add rules to your .gitignore to achieve this.
    4. Place an empty .gitkeep file inside temp_outputs/.
    5. Create another file, e.g., temp_outputs/report.tmp.
    6. Run git status. Verify that temp_outputs/.gitkeep is shown as untracked (or can be added), but temp_outputs/report.tmp is ignored.

Summary

  • .gitignore files specify intentionally untracked files that Git should ignore.
  • Patterns follow glob syntax (*, ?, **, / for directory, ! for negation).
  • .gitignore can be placed in the repository root or subdirectories. Rules are relative and can be overridden.
  • A global .gitignore (configured via core.excludesFile) handles user-specific ignores across all repositories.
  • .gitignore only affects untracked files. To ignore an already tracked file, use git rm --cached <file>, then add it to .gitignore, and commit.
  • Debug ignore rules with git check-ignore -v <path>.
  • The .gitkeep convention (placing an empty .gitkeep file) is used to make Git track otherwise empty directories.

Properly configured .gitignore files are essential for maintaining a clean and manageable Git repository by ensuring that only relevant project files are version controlled.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top