So it seems this is a common problem and there are plenty of ad hoc solutions, but I'd like to understand why git is built this way in general.
Problem: I clone a repository where files have a certain permission, and immediately git status shows that a ton of my files have "changed", due to the permissions being different. As I mentioned I'm not looking for an ad hoc fix or to change the way I do my default linux permissions, I'm looking to understand why git does this and what the underlying root of the problem is.
For example for any other changes I haven't yet committed, I can do a git checkout to revert the changes, but when I do this it makes no change to the file's permissions. If git is actually tracking my permissions and considers a permission change a change to the file, why can I not do git checkout in order to revert back to the initial permissions? And if git is tracking permissions, what would be the purpose other than for me to be able to change my permissions and make it such that other people who clone or fork my repository would create files with the same permissions?
I guess my question boils down to, if git is tracking permission changes as file changes, why doesn't git let me use any of the normal commands which are used to make sure my project is up to date with other collaborators on permissions just as it does with content? Why do I need to go through an ad hoc bandage to avoid my repository being filled with file permission change commits every time the person committing has different default file permissions set up? And if this is a production project what can we do to make it such that permission change commits actually make their way into the project? For example say we want to restrict a file so that only the group can read it in production, why couldn't a developer make this change and commit/push/pull request as they would fulfilling any other requirements of the project? This is all on linux btw so I'm not discussing committing from different file systems.
-
2I don't think the problem is in git. If the changes are related to permissions, then the problem is probably in the underlaying FS and how it is mounted. I saw this kinds of problem when working on Windows, and I checked out a project with git for windows and then I went into the project with git on cygwin (or viceversa).eftshift0– eftshift02019年12月03日 15:28:35 +00:00Commented Dec 3, 2019 at 15:28
1 Answer 1
Git only keeps one permission bit for files. For the most part, Git only stores files—it doesn't store directories (or folders if you prefer that term), it just creates them on demand instead. While Git does store symbolic links, symbolic links don't have associated permissions—at least not in Git (and usually not in the OS either).
More concretely, when Git stores a file, it stores it with a "mode". That mode is either exactly 100644 or 100755.1 This mode entry goes into the index—the thing that Git uses to build up the next commit—and the files in your work-tree, which are the ones you can see and work with, are set to be either executable, or not-executable, using a chmod system call, provided your OS and file-system actually support chmod operations.
When Git sets these mode bits—if it can—it sets the +x bit according to your umask, if you're on a Unix or Linux type system. The umask tells the system what bits not to set: a umask of 002 means that files are rwxrwxr-x or rw-rw-r--, because 002 gets cleared. A umask of 007 means that files are rwxrwx--- or rw-rw---- because 007 gets cleared: we take away read, write, and execute permission for "other". (The three fields are "owner", "group", and "other", with 4 = read, 2 = write, 1 = execute.)
So: the index holds the +x or -x bit, by storing a mode of 100755 (+x) or 100644 (-x). This is true on all systems, including Windows. Meanwhile the work-tree holds whatever the OS and file system allow. On Windows, this is commonly nothing at all.
If the work-tree holds nothing, Git:
- makes a note of that at the time you create the repository, and
- just leaves the mode stored in the index alone.
If the mode stored in the index came out of some existing commit—as is pretty typical—then it's presumably correct, and Git should leave it alone, so it does.
New files added to the index get (I think, I have not tested and don't run Windows) mode 100755 just in case, but you can use git update-index --chmod=+x or git update-index --chmod=-x to change it: +x means 100755 and -x means 100644.
But if the work-tree holds useful mode bits—if you can run chmod +x somefile or chmod -x somefile to change the execution permission of somefile in your work-tree—then Git has made a note of that, back when you first created your repository, and now git add will copy that +x or -x state into the 100755 or 100644 setting in the index. Meanwhile, git checkout updates the actual mode, stored in the work-tree, as needed.
1Long ago, Git stored more bits. This turned out to be a mistake and was changed, so that now it stores only the 644 or 755 mode. (The 100 at the front means file, and the remaining bits are the Linux-style permissions: 644 means rw-r--r-- and 755 means rwx-r-x-r-x.) There are a few repositories that have internal tree objects with mode lines that contain 100664, though, so git fsck secretly allows a few extra modes.
Where this goes wrong
As I mentioned above, Git probes the actual file system at the time you run git init (or git clone, which runs git init internally, more or less). That is, Git is creating a new repository. This new repository needs to know: If I set executable bits on files, will the OS remember that correctly?
So Git actually sets or clears executable bits on a file in the OS-provided file system.2 If the setting and/or clearing "sticks", Git knows that the OS handles executable bits correctly.
If the OS does handle +x and -x correctly, Git will set core.filemode in the new repository to true. If it does not handle +x and -x correctly, Git will set core.filemode in the new repository to false.
Later, Git will consult core.filemode to know whether the OS's chmod actually works correctly on the work-tree. If you move a repository from one file system to another, the recorded core.filemode may be incorrect. In this case, you can adjust core.filemode manually.
If some co-worker or colleague has a file system that doesn't handle chmod properly, and the co-worker fiddles with his or her core.filemode incorrectly, that co-worker will create new commits that have wrong mode entries. There is nothing you can do about this (well, except to educate them): A commit, once created, can never be changed. You can rip out the bad commit and put in a good replacement—with all the pain that comes with this—or just fix the modes in the next commit and not worry about it, but you'll have to get the colleague or co-worker to stop doing that.
If you have a co-worker who won't stop doing that and the permissions aren't really relevant to you, you can deliberately lie to your own Git. Simply set core.filemode to false so that your own OS's properly-tracked chmod settings are ignored. This is not a great solution, it's just a workaround until you can educate whoever's putting bogus mode bits into the repository.
2This file is the .git/config file that Git is building up for the new repository. The test can also be disabled at compile time: that is, someone can build a Git that assumes that chmod doesn't work, and just sets core.filemode to false.