gitgitignore

git ignore whole directory apart from files with a specific extension


I would like to have lines in my .gitignore file that ignore an entire directory and its subdirectories, aside from any files (and their folder structure) that end .pdf

At the moment i use;

outputs/*
!outputs/**/*.pdf

and this excludes the outputs directory and then seems to include a random selection of subdirectories and .csv's and .png's.

The folder structure is not fixed down to the pdf files (could be 1 or 8 subdirectories deep), but i just want .pdf files keeping only in that outputs/ folder. To be honest, i thought what i had was right!


Solution

  • I'll use this layout as an example.

    $ tree outputs/
    outputs/
    ├── foo.csv
    ├── foo.pdf
    └── subdir
        ├── bar.csv
        ├── bar.pdf
        └── subdir2
            ├── baz.csv
            ├── baz.pdf
            └── subdir3
                ├── woof.csv
                └── woof.pdf
    

    We can see exactly how Git is interpreting the ignore file with git check-ignore -v.

    $ cat .gitignore
    outputs/*
    !outputs/**/*.pdf
    
    $ git check-ignore -v outputs/**
    .gitignore:1:outputs/*  outputs/
    .gitignore:1:outputs/*  outputs/foo.csv
    .gitignore:2:!outputs/**/*.pdf  outputs/foo.pdf
    .gitignore:1:outputs/*  outputs/subdir
    .gitignore:1:outputs/*  outputs/subdir/bar.csv
    .gitignore:1:outputs/*  outputs/subdir/bar.pdf
    .gitignore:1:outputs/*  outputs/subdir/subdir2
    .gitignore:1:outputs/*  outputs/subdir/subdir2/baz.csv
    .gitignore:1:outputs/*  outputs/subdir/subdir2/baz.pdf
    .gitignore:1:outputs/*  outputs/subdir/subdir2/subdir3
    .gitignore:1:outputs/*  outputs/subdir/subdir2/subdir3/woof.csv
    .gitignore:1:outputs/*  outputs/subdir/subdir2/subdir3/woof.pdf
    
    

    The problem is outputs/* ignores all subdirectories of output/ and Git doesn't bother to look inside ignored directories. We need to unignore the subdirectories of outputs/ with !outputs/**/.

    $ cat .gitignore
    outputs/*
    !outputs/**/
    !outputs/**/*.pdf
    
    $ git check-ignore -v outputs/**
    .gitignore:2:!outputs/**/   outputs/
    .gitignore:1:outputs/*  outputs/foo.csv
    .gitignore:3:!outputs/**/*.pdf  outputs/foo.pdf
    .gitignore:2:!outputs/**/   outputs/subdir
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/bar.pdf
    .gitignore:2:!outputs/**/   outputs/subdir/subdir2
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/subdir2/baz.pdf
    .gitignore:2:!outputs/**/   outputs/subdir/subdir2/subdir3
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/subdir2/subdir3/woof.pdf
    

    Whoops, we also unignored the CSV files in the subdirectories! This is because outputs/* only ignores the first level of outputs/. To ignore everything under outputs/ change outputs/* to outputs/**.

    $ cat .gitignore
    outputs/**
    !outputs/**/
    !outputs/**/*.pdf
    
    $ git check-ignore -v outputs/**
    .gitignore:2:!outputs/**/   outputs/
    .gitignore:1:outputs/** outputs/foo.csv
    .gitignore:3:!outputs/**/*.pdf  outputs/foo.pdf
    .gitignore:2:!outputs/**/   outputs/subdir
    .gitignore:1:outputs/** outputs/subdir/bar.csv
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/bar.pdf
    .gitignore:2:!outputs/**/   outputs/subdir/subdir2
    .gitignore:1:outputs/** outputs/subdir/subdir2/baz.csv
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/subdir2/baz.pdf
    .gitignore:2:!outputs/**/   outputs/subdir/subdir2/subdir3
    .gitignore:1:outputs/** outputs/subdir/subdir2/subdir3/woof.csv
    .gitignore:3:!outputs/**/*.pdf  outputs/subdir/subdir2/subdir3/woof.pdf
    

    And now it works.

    $ git add outputs/
    
    $ git status
    On branch main
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
        new file:   outputs/foo.pdf
        new file:   outputs/subdir/bar.pdf
        new file:   outputs/subdir/subdir2/baz.pdf
        new file:   outputs/subdir/subdir2/subdir3/woof.pdf
    

    However, I would instead recommend not mixing up build artifacts with generated files you want to keep. Instead, change your build process to write those PDF files somewhere else. Then your .gitignore becomes simpler, and cleaning up outputs/ is simply rm -rf outputs/*