Settings

Theme

Don't create .gitkeep files, use .gitignore instead (2023)

adamj.eu

202 points by frou_dh 2 months ago · 100 comments

Reader

jkubicek 2 months ago

I'm not sure if I'm the one to blame for this or not, but the earliest reference to ".gitkeep" I can find online is my 2010 answer on Stack Overflow: https://stackoverflow.com/a/4250082/28422

If this is all my fault, I'm sorry.

  • pilaf 2 months ago

    This Rails commit from May 2010 mentions gitkeeps and it's a few months older than your SO post, so it seems you're absolved from guilt:

    https://github.com/rails/rails/commit/785493ffed41abcca0686b...

  • kazinator 2 months ago

    Dummy empty files such as .keepme were used in CVS repositories for exactly the same purpose, and probably other version controls systems long before Git existed.

    The Peter Cederqvist manual recommended the practice.

    Here is a 1993 dated copy someone left hosted:

    https://www.astro.princeton.edu/~rhl/cvs/cvs.html

    The paragraph which recommends the .keepme files is:

    https://www.astro.princeton.edu/~rhl/cvs/cvs.html#SEC63

    "if you want an empty directory then put a dummy file (for example `.keepme') in it to prevent `-P' from removing it."

  • hn92726819 2 months ago

    Yeah... I don't think you were wrong. Having 100 tiny gitignores makes finding out why something is excluded annoying. Our policy is one root level gitgnore and gitkeeps where required.

    Some devs will just open the first gitignore they see and throw stuff into it. No thank you.

    • zahlman 2 months ago

      I like to make a .local folder at the top of the project, which contains a .gitignore that ignores everything. Then I can effortlessly stash my development notes there without affecting the project .gitignore or messing around within the .git directory.

      • ddek 2 months ago

        You can create a global gitignore in your home directory. I have ‘.<myname>’ ignored there, so if I ever create a directory with that name I know it’s contents won’t go into source control. That way I don’t have to edit the repositories gitignore with me-specific stuff.

        • hobofan 2 months ago

          You wouldn't have to edit the actual repositories gitignore anyways. Every checkout of a repo comes with a .git/info/exclude file, which acts like a local additional gitignore file.

      • beAbU 2 months ago

        Why not put '.local' in your toplevel gitignore, and not commit an empty .local folder up to the forge?

        • zahlman 2 months ago

          Upstream never sees an empty .local folder because, as established, Git doesn't keep empty folders. This way, .local isn't mentioned in the top-level .gitignore. It's just that tiny bit cleaner.

    • predkambrij 2 months ago

      I share your view. .keep and .gitignore are different things. Having one .gitignore caputuring everything is less mental load.

    • taftster 2 months ago

      I agree with you. Empty .gitignore would be a "smell" to me. Whereas .gitkeep tells me exactly what purpose it serves. I like the semantic difference here that you describe. I don't like when multiple .gitignore files are littered throughout the codebase.

    • nickysielicki 2 months ago

      > Having 100 tiny gitignores makes finding out why something is excluded annoying. Our policy is one root level gitgnore and gitkeeps where required.

      This is not a complicated or important enough problem to justify a team-wide policy. Let it work itself out naturally.

      https://git-scm.com/docs/git-check-ignore makes it trivial to debug repo-wide gitignore behavior.

      • lucketone 2 months ago

        We could say that practically all problems would work them self out one way or another.

        I heard facebook allows any language as long as you have packaged it neatly with all required build chain. (I.e. Even a choice of language works out)

        Some lowest common denominator will be reached. (1 :) )

        On the other hand: Are we happy with the lowest or do we want to aim higher?

  • selridge 2 months ago

    This is delightful. Accidental load-bearing SO post.

    • jkubicek 2 months ago

      It's especially funny since my answer is wrong anyway! The other top answer is much better. I did get a lot of early SO brownie points from that one answer though.

Arrowmaster 2 months ago

The author makes a very common mistake of not reading the very first line of the documentation for .gitignore.

  A gitignore file specifies intentionally untracked files that Git should ignore. Files already tracked by Git are not affected; see the NOTES below for details.
You should never be putting "!.gitignore" in .gitignore. Just do `echo "*" > .gitignore; git add -f .gitignore`. Once a file is tracked any changes to it will be tracked without needing to use --force with git add.
  • BlackFly 2 months ago

    The point of that line is to robustly survive a rename of the directory which won't be automatically tracked without that line. You have to read between the lines to see this: they complain about this problem with .gitkeep files.

  • ekipan 2 months ago

    Yeah, this. Plus a mistake from the article:

      $ echo '*\n!.gitignore' > build/.gitignore
    
    The \n won't be interpreted specially by echo unless it gets the -e option.

    Personally if I need a build directory I just have it mkdir itself in my Makefile and rm -rf it in `make clean`. With the article's scheme this would cause `git status` noise that a `/build/` line in a root .gitignore wouldn't. I'm not really sure there's a good tradeoff there.

    • Aaron2222 2 months ago

      > The \n won't be interpreted specially by echo unless it gets the -e option.

      Author's probably using Zsh, which interprets them by default.

      • _kst_ 2 months ago

        If you want any kind of non-trivial formatting, use the printf command, not echo.

  • AgentME 2 months ago

    If you have a project template or a tool that otherwise sets up a project but leaves it in the user's hands to create a git repo for it or commit the project into an existing repo, then it would be better for it to create a self-excepting .gitignore file than to have to instruct the user on special git commands to use later.

  • xg15 2 months ago

    I think I'd prefer to have all ignores and un-ignores explicitly in the file and not have some of them defined implicitly because a file was added to tracking at some point.

    • 1718627440 2 months ago

      But ignore files are only for untracked files anyway. Maybe you want them to specify what can be in the repo and what not, but this is not how Git works.

  • nebezb 2 months ago

    This is functionally the same. What do you mean by “you should never”? According to who?

    What an arrogant take. This is preference. Don’t mistake it for correctness.

  • smrq 2 months ago

    Why is this approach better than the author's?

hollasch 2 months ago

My preference is to use the build system to create built artifacts, and I consider the build/ directory to be a built artifact. Wrangling Git into doing the first fundamental build step is off, in my opinion.

However, if you disagree, my favorite "Git keep" filename is "README.md". Why is this otherwise empty directory here, how does it fit into my source tree, how is it populated, and so forth.

One of my pet peeves with the latest AI wave is the time we spend creating files to help AI coding agents, but don't give the same consideration to the humans who have to maintain and update our code.

  • taftster 2 months ago

    Both points here are appreciated. One that a README file as a "placeholder" for a directory gives the opportunity to describe why said empty directory exists. I would be slightly concerned though if my build process picked up this file during packaging. But that's probably a minor concern and your point stands.

    Additionally, the AI comment is ironic as well. It's like we're finally writing good documentation for the sake of agents, in a way that we should have been writing all along for other sentient consumers. It's funny to see documentation now as basically the horse instead of the cart.

GreenDolphinSys 2 months ago

.gitkeep is intuitive and easy to understand. Unignoring a .gitignore is not intuitive. This falls squarely into "clever optimization tricks that obscure intent and readability". Don't do things like this.

It's not that hard to update a .gitignore file every now and then.

  • rswail 2 months ago

    Then put a comment in the .gitignore.

    Using the actual tools built in to git directly removes steps in the process, which is always a good thing, it's documented as part of the git documentation, so you don't have to create a wiki page explaining why there is a ".gitkeep" file that git doesn't recognize itself.

    Saying "It's not that hard..." is fine for projects with a few contributors but does not scale.

    • trelbutate 2 months ago

      If someone doesn't know what .gitkeep is they should be able to derive from the name that it's some special file intended for git. If they then google it they will immediately find out what it's for. Yes, git itself has no concept of it but it's common enough that there's plenty documentation on the internet.

      • rswail 2 months ago

        Yes, because relying on google-fu is the way to ensure your build environment is consistent. /s

        .gitkeep is explicitly not intended for git, because git doesn't recognize it at all.

        Having the .gitignore, which is actually recognized by the git tools, means you can rely on the .gitignore functionality, including ensuring that things other than the .gitignore cannot be added to the repo.

  • throwaway290 2 months ago

    .gitkeep is not a standard and also because it starts with dot it is hidden by default in file listings which makes it even less intuitive.

cortesoft 2 months ago

Not sure why you can’t just have your build script create the build directory?

  • andybak 2 months ago

    Because you might not have a build script?

    • cortesoft 2 months ago

      Then how is anything ending up in the build directory?

    • drdec 2 months ago

      Then why do you need a build directory?

      • himata4113 2 months ago

        qemu: mkdir build; cd build; ../configure, some projects are like that

        • xigoi 2 months ago

          Why can’t the configure script do this?

          • 1718627440 2 months ago

            Because the standard for configure scripts says that the current directory at invocation is the build directory and the location of the configure script is the source directory. You are expected to be able to have multiple build directories if you want them. If you have written your configure script correctly, than in-tree builds (srcdir == builddir) also work, but most people don't want that anyway.

          • hn92726819 2 months ago

            You can. But this makes intent clear. If you clone a git repo and see build/ with only a gitkeep, you are safe to bet your life savings on that being the compiled assets dir.

  • twic 2 months ago

    Usually, you can. But occasionally you get mildly defective tools that require some directory to exist, even though it's empty. It's easier to add a gitkeep than fix them.

    • taftster 2 months ago

      This used to happen a lot. But I don't think that many modern builders require existing directory these days.

      Your point is valid though. It would be much preferable to include build/ in your root .gitignore so that the directory is never tracked.

  • xg15 2 months ago

    There may be other directories. I think it's useful to be able to see the entire directory structure of a repo when you check it out, and not just after running some scripts.

beej71 2 months ago

What am I missing about this use case? It seems like you should just create `build/.gitignore` with `*` in it and `add -f` it and be done.

I'd use `.gitkeep` (or an empty `.gitignore`) if I needed to commit an otherwise-empty hierarchy. But if I'm going to have a `.gitignore` in there anyway, it's not empty.

> The directory is now “tracked” with a single, standard file that will work even after renames.

Does `.gitkeep` not work after renames? Or `.gitignore`?

So I am missing something. :)

  • KPGv2 2 months ago

    That's a hack. What you should do is a .gitignore with * and then a whitelist of paths like src/**/*.

    If you rely on `add -f` you will forget to commit something important.

    For example, for a tree sitter grammar I developed a couple years ago, here is my .gitignore:

    ```

    # Ignore everything

    *

    # Top-level whitelist

    CHANGELOG.md

    # Allow git to see inside subdirectories

    !*/

    # Whitelist the grammar and tests

    !/grammar/*.js

    !/test/corpus/*.txt

    # Whitelist any grammar and tests in subdirectories

    !/grammar/**/*.js

    !/test/corpus/**/*.txt

    ```*

    • beej71 2 months ago

      > If you rely on `add -f` you will forget to commit something important.

      But isn't the idea in TFA to blacklist the entire `build/` tree? We don't want to add anything there.

      • KPGv2 2 months ago

        I have much more than `build` to block. IDE-specific stuff, smalls scripts I write to help me (and go in root) but don't belong in the repo, etc.

        And what if for some reason you accidentally copied a big Linux ISO to that directory by mistake. Without a whitelist, you might accidentally add and commit a 700MB file to your main and not even notice. What a pain when you push later and have to git amend, rebase -i, etc.

        Better to block all except whitelist. The only downside is it's less obvious how to do this than allowing all except blacklist to new git users.

  • aezart 2 months ago

    It makes the behavior more obvious from simply looking at the file, for one thing, and it means you can just lump it into your next `git add -A` without needing to handle it specially.

Kuraj 2 months ago

If you need to do this, I think .gitkeep communicates intent better. You don't need to document it or risk it being removed as thought to be a left over.

prmoustache 2 months ago

How about fixing your build scripts and makefiles instead? Convoluted solutions for a non-existing problem.

kderbyma 2 months ago

Arent Gitkeep files specifically for empty folders that are intended to be there?

That is what I have always used them for....

  • wlonkly 2 months ago

    .gitkeep has no meaning to Git, only to us meatsacks that recognize the name. As far as Git is concerned, it's just another file.

8cvor6j844qw_d6 2 months ago

Is .gitkeep an established convention somewhere? I'm curious where the name originated.

Joker_vD 2 months ago

Why did Git decide to have no means to track fully empty directories? Like, I understand that e.g. doing "git rm *" inside a directory should probably delete this directory from the repository as well (although "git rm -r dir_to_delete" exists so...) but why not have a command to explicitly force a directory to be tracked, whether it's empty or not?

zahlman 2 months ago

Oh, man, I'd forgotten about these negated .gitignore patterns entirely. It actually hadn't occurred to me that they could override the behaviour of ignoring empty directories.

This is potentially actually useful for me, because I have a project with test data that consists of miniature filesystem sub-trees — that should include empty directories to ensure edge cases are covered. I've been zipping them up and having the test harness unpack them in the test environment, but that's an unnecessary extra point of failure (and it stuffs undiffable binary files into the commit history).

Edit: Ah, no, if this doesn't work from the project-global .gitignore (specifying a folder to keep, even though it's empty and doesn't even have its own .gitignore) then it doesn't solve the problem. :(

  • ivan_gammel 2 months ago

    Why not having a txt file with indented tree, a bootstrap function that parses the file and creates the tree and a test for that function?

    You will have proper diff for the tree this way.

    • zahlman 2 months ago

      Well, partly because I already have a tool similar to that in mind as a separate project :) (The files also need to have certain content, so I need something a bit more complex that can specify that.)

      But mainly because, as you acknowledge, that process needs its own tests. Basic unzipping functionality is already tested for me.

OptionOfT 2 months ago

For me, I put them in directories that have to be there, because the underlying code doesn't create the directory, and without it, it fails.

Another example is where you want an empty directory mounted in Docker. If the directory is not there it is created with root permissions and then I can't even look into it.

JuniperMesos 2 months ago

Maybe the correct thing to do is to modify git so that it can track an otherwise-empty directory and then never have to worry about this problem again. Is there a reason why this is hard to integrate into git's existing model of the filesystem?

  • lucketone 2 months ago

    1. Nobody sees this as a big issue.

    2. As-is situation is good enough.

    3. I guarantee that you will have colleagues that will accidentally add empty dirs. (I definitely have seen when they accidentally add built artefacts; dirs would be lesser issue as they would not increase repo size by much, but still)

    4. Actually only first two reasons matter, but for nitpickers there would be also a bit dishonest but formally correct pointing to some doc saying that it is file or content tracker (and formally directory is not a content)

  • 1718627440 2 months ago

    Git only tracks files not directories, so yeah you would need to change a lot. It is also unclear what the semantics should be. What is with a directory that is tracked as r--, but then files get added?

deafpolygon 2 months ago

Claims the wrong thing is common and tells you not to do it , then tells you to do the right thing.

I have never heard of .gitkeep before today, and if you need an empty directory to exist, use a build script.

Don’t do stupid workarounds.

suralind 2 months ago

I want to like it, but I pretty much always have a "cleanup" script that just deletes the entire directory and touches a .gitkeep file. Obviously an even better pattern is to not have any .gitkeep files, but sometimes they are just handy.

jiffygist 2 months ago

I don't understand why would you ever want to have an empty directory. Besides if I see a directory named "build" I expect to be able to just nuke it any time without consequences.

macote 2 months ago

The author is misusing .gitkeep. I use it to keep source code folders that don’t contain any code yet, but whose structure is already defined.

  • xyzzy_plugh 2 months ago

    Truly, what purpose does this serve? Defining a hierarchy without using is injecting immediate debt. Just introduce it when stuff goes there! If you really insist then at least put something in the folder. It doesn't take much effort to make the change at least a tiny bit meaningful.

    Better yet just do the work. If you want make a commit in a branch that's destined to be squashed or something, sure, but keep it away from the shared history and certainly remove it when it's not needed anymore.

    • abustamam 2 months ago

      I play around with ComfyUI on my computer to make silly images.

      To manually install it, you must clone the repo. Then you have to download models into the right place. Where's the right place? Well, there's an empty directory called models. They go in there.

      IMO that's an effective use of gitkeep.

    • akoboldfrying 2 months ago

      > Truly, what purpose does this serve?

      The simplest answer is that sometimes other existing software that I need to use treats an empty directory (or, hopefully, a directory containing just an irrelevant file like .gitkeep) differently from an absent directory, and I want that software to behave in the first way instead of the second.

      A more thorough answer would be: Filesystems can represent empty directories, so a technology that supports versioned filesystems should be able to as well. And if that technology can't quite support fully versioned filesystems -- perhaps because it was never designed with that goal in mind -- but can nevertheless support them well enough to cover a huge number of use cases that people actually have, then massaging it a bit to handle those rough edges still makes sense.

      • xyzzy_plugh 2 months ago

        Legitimately asking, please share the name of software that expects/requires an empty directory and interprets .gitkeep in this way, but chokes on a README file.

        Many filesystems cannot represent empty directories. Many archive formats also do not. I don't think this a problem in practice. I find this argument extremely weak.

        • akoboldfrying 2 months ago

          > please share the name of software that expects/requires an empty directory and interprets .gitkeep in this way, but chokes on a README file.

          Every mail server since postfix supports Maildir, in which every email is a file in one of 3 subdirectories (tmp, new or cur) of a user directory. If there's no jbloggs/tmp dir, postfix will think user jbloggs does not exist. So if you want to take a restorable snapshot of this with git, there needs to be a file there. I don't know if jbloggs/tmp/README would cause a problem, because I don't know how postfix will treat the presence of a file with a name that violates its expected syntax (which includes a timestamp and other metadata), but what I do know is that, after running `git clone any-repo`, I can safely delete every .gitkeep file to restore the system to its original state without having to understand that repo in detail -- while I cannot safely delete every README file. That's because the commonly understood semantics of .gitkeep is "I exist solely to shoehorn this empty dir into git", which does not apply to other files.

          > Many filesystems cannot represent empty directories

          Your turn -- name one.

        • Joker_vD 2 months ago

          > Many filesystems cannot represent empty directories.

          Like which ones? And how does mkdir(1) work on such filesystems?

  • CGamesPlay 2 months ago

    You can rename `.gitkeep` to `.gitignore` and both be happy in that case.

globular-toast 2 months ago

I find this use of .gitignore far more common than .gitkeep. I did see one js tool creating them and did wonder what it was about.

darepublic 2 months ago

In some cases gitkeep can make sense. Just let people choose the right tool

dmarinus 2 months ago

if possible you can also just create directories if they don't exist (ie. mkdir -p) and just exclude it in your root .gitignore (ie. ignore all build directories). That would safe you from creating multiple .gitignore files.

yjftsjthsd-h 2 months ago

I'm confused. Having a file gitignored doesn't stop you from committing it; AFAIK you can just

  touch build/.gitkeep
  git add build/.gitkeep
  git commit build/.gitkeep
And that's it? There's no need to exclude anything.
  • williadc 2 months ago

    The idea is that you don't want to check-in any builds.

    • yjftsjthsd-h 2 months ago

      Sure, so gitignore build/ or whatever. But you don't need to unignore .gitkeep

      • akerl_ 2 months ago

        The idea is that instead of adding a nonsense file, you use the native .gitignore functionality.

        ".gitkeep" is just a human thing; it would work the same if you called it ".blahblah".

        So their pitch is that if you want to explicitly keep the existence of the directory as a committed part of the repo, you're better off using the actual .gitignore functionality to check in the .gitignore file but ignore anything else in the directory.

        I don't find it amazingly compelling; .gitkeep isn't breaking anything.

cyberrock 2 months ago

File filtering is so delightfully broken everywhere. Everytime I revisit git, rsync, restic, borg, etc. something just goes wrong somewhere on this seemingly simple task, and SO and thus LLMs are filled to the brim with slightly wrong answers. We need a xkcd/927 because it can't possibly get any worse.

leecommamichael 2 months ago

This doesn’t solve a problem.

phelm 2 months ago

could you just add "!.gitkeep" to .gitignore?

peter-m80 2 months ago

No, thanks

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection