Skip past (full) staging area and commit file or p

2019-07-08 11:41发布

问题:

Imagine this scenario: you're working on a feature that requires touching lots of files, and you've got a lot of things staged, and a lot of things not staged (like debugging code, temporary comments for yourself to remember to do/undo certain things and not forget to add bits that you didn't yet have time to add), and then you see a simple one-line change that you must make, but which belongs in its own commit.

Is there any way to simply commit that without pulling everything out of the staging area that you've meticulously added, without stashing (and risking losing your careful selections of what to stage and what not to stage) and just commit that one line?

I realize that fiddling with multiple staging areas would probably make this possible, but I'm hoping there is a simpler solution than that. Some switch that lets me skip the staging area would be more convenient than mucking about with GIT_INDEX_FILE to have 2 of them.

My ideal solution would be something sort of like this:

git commit --skip-stage --patch ./app/models/whatever.rb

If this is impossible, then I will simply stash and use --index when I pop it back, and hope I didn't accidentally do something between stashing and popping that breaks the ability to cleanly restore the index.

Because I know someone will wonder "if you know about --index with git stash pop, why are you asking this question? Because it is as much about pushing the envelope of what I can do with Git as it is about solving a practical problem. Just because one solution to a problem exists doesn't mean it is the best solution or that one should stop looking for alternatives. That applies to all of life, not just Git.

回答1:

There is the -a flag, git commit -a, and the --only and --include flags (which can be shortened to -o and -i) that allow you to do:

git commit --only file1 file2

or:

git commit --include file3

but these work by creating a new temporary index, as I outlined in my answer to your linked question.

What these do is a little bit magic, and may or may not be what you want. In particular, the temporary index file(s) that these create include .git/index.lock, which is Git's internal temporary new index, which—if the commit succeeds—will become the (regular) index afterward. This has a some very powerful (and somewhat peculiar) consequences. Let's look at each mode of operation. The --only variant comes close to what you want, and might sometimes be what you want, but might sometimes destroy something valuable.

git commit --only file1 file2

This starts by making a new temporary index that is copied from HEAD. Into this new temporary index, Git copies file1 and file2 from the work-tree, as if by git add file1 file2. So now this temporary index matches HEAD except for the two named files.

Git also creates .git/index.lock by copying the current index contents, i.e., the staged files. Into this temporary index, Git copies file1 and file2 as before. So this temporary index—which is different from the first one—has all your staged files except that file1 and file2 are overwritten from the work-tree.

Now Git makes a new commit, using the first temporary index—the one that mostly matches HEAD, except for the two files. If this commit succeeds, Git updates the current branch as usual, then removes this first temporary index, and unlocks and updates the real / regular index by renaming the second temporary index, .git/index.lock, to .git/index. So now the normal index is the way it was before except that file1 and file2 have been replaced in it, as if by git add file1 file2.

If you had carefully staged a different version of file1 and/or file2, which are (of course) not in your work-tree right now—because the work-tree versions are the ones you're committing with --only—that special version is gone, wiped out by the git add step. If not, this is probably what you wanted!

git commit --include file3

Here, let's assume that you've git added file1 earlier—perhaps a slightly different version of file1 than is now in your work-tree, so that file1 in the real / regular index differs from file1 in HEAD.

Git starts by making a new temporary index named .git/index.lock, copied from the real / regular index. Then, Git copies file3 into this temporary index, as if by git add file3.

Now Git makes a new commit, using the temporary .git/index.lock index. If this commit succeeds, Git updates the current branch as usual, then renames the temporary index from .git/index.lock to .git/index. So now the real / regular index still has the file1 you added earlier (which perhaps doesn't match the file1 in the work-tree but is different from the old commit's file1), plus the file3 you added via git commit --include.

(This mode never destroys any carefully-staged files, but also doesn't do what you describe in your question.)