Search code examples
gitgitignore

Do git update-index --assume-unchanged rules propagate to clients on pull?


There are instances where I can't use a .gitignore file, otherwise, on git push, critical files are purged from the remote. In these cases, I apply git update-index --assume-unchanged <file> to the files that I want to ignore.

After applying the assume-unchanged rules and calling git push, will these rules be attached to the remote branch so that all subsequent pulls (from other clients) will inherit them? Or, must these clients also run the git update-index --assume-unchanged <file> commands individually at their machines?

If the commands are not inherited -- has anybody written a server hook for this before? Instead of mandating that all current and future clients safeguard against it?


Solution

  • The index is local to your workstation, any changes you make there do not propagate to other clones of the same remote.

    (updated, after question was updated)

    The .gitignore file

    There are instances where I can't use a .gitignore file, otherwise, on git push, critical files are purged from the remote.

    This is not true. The git ignore file does not affect files that are already tracked in the repository. If you add a file to .gitignore after it has been committed, it'll stay inside the repository; it will not be purged. In fact, it behaves pretty much as if it weren't ignored at all.

    You can easily check this in a temporary repository:

    $ mkdir -p /tmp/repo
    $ cd /tmp/repo
    $ git init
    Initialized empty Git repository in /tmp/repo/.git/
    $ echo red > a.txt
    $ git commit -am '1'
     1 file changed, 1 insertion(+)
     create mode 100644 a.txt
    $ echo a.txt > .gitignore
    $ echo b.txt >> .gitignore
    $ git commit -am '2'
     1 file changed, 2 insertions(+)
     create mode 100644 .gitignore
    

    The repository now contains two files: a.txt and .gitignore. Both behave normally, which you can see when you clone it:

    $ cd ..
    $ git clone file://repo repo2
    $ ls -A repo2
    .git       .gitignore a.txt
    $ cd repo
    

    If we modify both ignored files and request git status, we'll see that a.txt is seen as modified, despite having been gitignored. We can add and commit it as normal; in fact, if you add an tracked file to gitignore, it behaves pretty much like it's not in gitignore at all.

    $ echo green > a.txt
    $ echo blue > b.txt
    $ git status --short
     M a.txt
    $ git add a.txt
    

    The b.txt file is different, because it was ignored before git started tracking it. This file will not normally make it into the repository, but we can force it if we want to.

    $ git add b.txt
    The following paths are ignored by one of your .gitignore files:
    b.txt
    Use -f if you really want to add them.
    fatal: no files added
    $ git add -f b.txt
    

    Issuing git commit now commits two files that have both been git ignored:

    $ git commit -m '3'
     2 files changed, 1 insertion(+), 1 deletion(-)
     create mode 100644 b.txt
    

    Long story short, think of git ignore rules as guidelines :-)

    Propagating assume-unchanged

    After applying the assume-unchanged rules and calling git push, will these rules be attached to the remote branch so that all subsequent pulls (from other clients) will inherit them? Or, must these clients also run the git update-index --assume-unchanged commands individually at their machines?

    The latter. Closest you can get is add a shell script to the repository that makes the changes for you.

    The server hook?

    If the commands are not inherited -- has anybody written a server hook for this before? Instead of mandating that all current and future clients safeguard against it?

    If your aim is to write a server hook that removes the critical files as if they weren't part of the push at all, that's not possible. Git push deals primarily with commit objects (and refs). Their dependent objects, like trees and blobs, are transferred as needed, subject to reachability from a commit. What it boils down to is that, if it's not committed, you can't push it to a remote (that's an oversimplification, but it holds true for the files in the repository). In addition, git commits are cryptographically guarded. You can't change a commit without changing its commit hash, and if you change the commit hash, you basically have a different, new commit (that may happen to have the same diff as the old one).

    This means that the server can't rewrite commits; at least not without seriously confusing the client that did the push (which will still have a copy of the old commit objects).

    What you can do is write a post-receive hook that refuses the commits if they contain the files you don't want to have updated. This doesn't really solve your problem, because if you have trouble explaining git commit --assume-unchanged to your coworkers, then you'll likely have even more trouble explaining how they can use interactive rebase to recreate their commits without the undesirable files in them.

    Long story short, I think chasing everybody to keep using assume-unchanged (maybe combined with a post-receive hook) is your least bad choice if you're dealing with files that should be committed once and never thereafter, like you have now.

    A possible work-around

    Your life would become a whole lot easier if you can keep these files out of git in their entirety. One of the things I can think of:

    • move the files within the repository to a lib directory, where they don't get modified all the time
    • .gitignore the files in their definitive location, the one where they have to be but get unwanted changes all the time
    • add an "init" script to your repository that people need to run once after cloning and before starting work. This script copies the files into the right location.