TL;DR
Set the --skip-worktree
bit for those particular paths in the index:
git ls-files -dz | xargs -0 git update-index --skip-worktree
(I assume your Windows-bash has xargs -0
; if not, you might have to fiddle with this a bit.)
Long(ish)
Your git update-index
command produced no errors, so it's not affecting these files, which means you're looking in the wrong place.
One fundamental issue here is that git push
does not push files. Instead, it pushes commits. It's true that commits contain files, so you might argue that this is a useless distinction—but it's a key distinction that Git makes, and it's at the heart of both the problem, and the cure.
... a few files had asterisks in their filename which apparently can't exist on a Windows system
That appears to be correct (What characters are forbidden in Windows and Linux directory names?). In one sense, it doesn't affect or bother Git, though, because Git deals with commits, not files. It affects you. :-)
Git stores commits in its repository database. Git also stores each version of each file in this database. Each stored item, called an object internally, has a special, compressed, Git-only form. The files inside a commit are additionally frozen that way for as long as the commit exists (typically forever).1
Frozen (read-only), Git-only files are useless to you and the rest of the programs on your computer. So Git needs a way to extract them into read/write, useful form. It does this in two stages: first it effectively thaws the files by listing them in its index (which is really just a file named .git/index
). The index keeps track of what is—or at least should be—in the work-tree. Then it de-compresses them and gives them useful names—real file names, in the real file system—and puts them in your work-tree.
It's the last step, of turning the thawed, but still Git-only, index entry into a file name like foo*bar
, that fails because of the forbidden name. So the thawed entry is in the index, but is not in the work-tree.
Git always makes new commits from whatever is in the index. Even commands like git commit -a
really just update the index, then build the commit from the index (though some use a temporary auxiliary index just for the duration of the commit, then fix up the real index). So until you change the copy of a file that's in the index, anything you do to the work-tree—including removing a file, or what Git thinks is removing since the file never got created—is just a change that is, in git status
's terms, not staged for commit.
What we'd like, though, is to avoid having git status
complain, and avoid having git add
remove the index copy of the file. That is, if there was a file foo*bar
, and it did not get into our work-tree, that's sort-of-OK, as long as we don't want to do anything with that file. We can leave the file in the index, missing from the work-tree, and have each new git commit
we make re-freeze the index copy. It's just annoying that we have to tiptoe around the fact that there's no foo*bar
in the work-tree.
Both of the index flags, --assume-unchanged
and --skip-worktree
, let us work around that fact. The --skip-worktree
is the flag that's meant for this purpose: the Git guys added it for "sparse checkout", and this is a special case of a sparse checkout. So we can just find the list of files that git status
will consider deleted from the work-tree—i.e., files that are listed in the index, are missing from the actual work-tree, and don't have the special flags set in the index—and set the --skip-worktree
flag.
git ls-files -d
prints a list of such file names. With -z
, it prints them terminated by an ASCII NUL (\0
) character instead of a newline. The xargs -0
command reads these NUL-terminated strings and runs git update-index --skip-worktree
on them, so now all those index entries have the flag set. New commits will contain the files (since the index lists them), but the work-tree won't (since it can't); meanwhile git add
won't try to remove them, and git status
won't complain about them.
1This is actually part of a general rule: Git's internal objects live in a key-value store addressed by keys that are hash IDs, and the hash ID of any object is a unique, cryptographic checksum of the object's content. The content cannot be changed because if it were changed, the checksum would change, after which the old key would still retrieve the old object, with only the new key / hash-ID able to retrieve the new object.