2

I've made a powershell script that adds a header to certain files within my project. I want to run it whenever code is in the process of being pushed to GitHub so that all such files in the GitHub repository have the header attached, removing the need to run the script manually.

The issue is that when I modify the files in my pre-commit git hook, those changes aren't restaged, so I essentially have to stage, commit and then stage again for the headers to be committed.

Is there a good way to deal with this, or is it better practice to separate the execution of that powershell script from the git flow?

user2805004
  • 193
  • 1
  • 1
  • 6

2 Answers2

2

TL;DR

The answer to the question in the subject line is "yes, but." :-) I'll rephrase it a bit here so that we can talk about all the technical details. The "but" is that there's a big surprise if you use git commit --only. I seem to recall that there was a time when it affected git commit -a badly too, but it's better about that now. (I'm not sure which Git versions behave badly with -a.)

Long

Git makes each new commit from an—or the, in most cases—index. The index, or any index, is essentially a proposed commit: it consists of a sort of flattened tree, with blob hash numbers and file names. It's in a form that makes it especially convenient for git mktree, which turns the index into a series of tree objects and returns the hash ID of the top level tree object, which then goes into the commit object.

The question, then, touches on these three parts:

  1. Precisely which index is being used to build the new commit?
  2. Can that index be modified? If so, what happens to the new commit?
  3. How does this affect the index?

Here, the index is the special, distinguished index that goes with the work-tree. There is one such index per work-tree: if you use git worktree add you get more work-trees, and each one gets its own private index, but you're only ever in one work-tree at a time, so "this work-tree's index" is the index.

However, when you run git commit, you can instruct it (using --only and/or other command line arguments) to build its own private index, separate from the index. If you have done so, it will run each of the various hooks with an environment variable, GIT_INDEX_FILE, set to the path name of the temporary index. (If not, GIT_INDEX_FILE will contain .git/index, which is the path to the index file.)

So the answer to Q1 is: $GIT_INDEX_FILE.

Now, you can in fact modify whichever index Git is using to build the commit, because Git will re-read this index after running your hooks. So the answer to Q2 is: Yes, and this makes the next commit use whatever's in whichever index is in use.

Q3 is the hardest. If you use git commit --only <paths>, Git must make a temporary index that:

  • is a copy of HEAD
  • except for the specified <paths>

without disturbing the index at all. If the commit goes on and succeeds, though, Git must now modify the index to account for the fact that these <paths> have new blobs in them in the new HEAD commit.

In fact, Git needs to make two temporary indexes (indices?), one for the proposed commit with its --only files, and one for the proposed commit result. The proposed commit result becomes index.lock, which acts as the new index.

If you git add files while running, they will go into the temporary index. But what about the index? Let's find out:

$ cat .git/hooks/pre-commit
#! /bin/sh
echo pre-commit hook, GIT_INDEX_FILE = $GIT_INDEX_FILE
git add sneaky
$ echo this is the base version of sneaky > sneaky
$ echo this is the base version of other > other
$ git add other sneaky
$ git commit -m 'create two files'
pre-commit hook, GIT_INDEX_FILE = .git/index
[master 5131b63] create two files
 2 files changed, 2 insertions(+)
 create mode 100644 other
 create mode 100644 sneaky

As you can see, the pre-commit hook fired here, and added sneaky. This is no big deal since what got copied into the index for this commit—which is the index—was already that same base version.

Now, however, let's modify the current contents of sneaky, git add it, and also modify the current contents of other, and git add it, so that we have new contents in the index for both files...

$ echo version 2 of other > other
$ echo version 2 of sneaky > sneaky
$ git add other sneaky

At this point, both index and work-tree have "version 2". Now let's update both work-tree files to "version 3" without git adding them:

$ echo version 3 of other > other
$ echo version 3 of sneaky > sneaky
$ git status --short
MM other
MM sneaky

This tells us that the HEAD, index, and work-tree versions all differ: the HEAD version of each is the base, the index version is version 2, and the work-tree version is version 3.

Now we run git commit --only other:

$ git commit --only other -m 'jump other straight to v3'
pre-commit hook, GIT_INDEX_FILE = [path]/.git/next-index-72393.lock
[master 91ec03b] jump other straight to v3
 2 files changed, 2 insertions(+), 2 deletions(-)

Let's see what we have in the commit, the index, and the work-tree now:

$ git status --short
MM sneaky

OK, the work-tree version of other matches the index version of other which matches the HEAD version of other, so:

$ cat other
version 3 of other

they are all version 3. What about sneaky, though? The HEAD and index differ, and the index and the work-tree differ. Let's see what's in HEAD first:

$ git show HEAD:sneaky
version 3 of sneaky

Now let's see what's in the work-tree:

$ cat sneaky
version 3 of sneaky

Aha, these match! The tricky part is viewing the index version, but we can do that too with git show:

$ git show :0:sneaky
version 2 of sneaky

Whoa, look at that! The index version is the old one!

This, then, is the answer to Q3: the git add in the pre-commit hook updates whichever index is being used to build the next commit, but if that's a temporary index, it does not update what will become the real index. This is arguably a bug: git add in the pre-commit hook should also add to the index that will become the index. Making that happen is a little tricky (git commit could read the temporary index before and after the hook and copy any updates to the index.lock file, perhaps).

Note that I skipped the details for what happens if you commit without --only. Fortunately, in this case the index you add to is the index, so everything works as you would expect:

$ git reset --hard 5131b63
HEAD is now at 5131b63 create two files
$ echo v2 > other && echo v2 > sneaky && git add other
$ git commit -m 'regular commit'
pre-commit hook, GIT_INDEX_FILE = .git/index
[master 10a8b20] regular commit
 2 files changed, 2 insertions(+), 2 deletions(-)
$ git status --short
$ 

The pre-commit git add replaced the index version of sneaky, and it stayed replaced ... which it will even if the commit fails.

Note also that when using git commit -a, we get a different temporary index:

$ git commit -a -m test
pre-commit hook, GIT_INDEX_FILE = [path]/.git/index.lock

Here, what Git does for git commit -a is to create the proposed new index as the index.lock file. The git add proceeds normally, adding the file in the usual way, and then when the commit succeeds, Git renames index.lock to index. This unlocks the index file and, at the same time, puts the modified index in place, so that the answer for git commit -a to Q3 is that the temporary index becomes the index. This is different from that for git commit --only.

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
torek
  • 448,244
  • 59
  • 642
  • 775
0

This is very similar to this post.

You want to use .gitattributes and specify scripts that run at add time (i.e. git add). This way, every time your file gets added, the script runs and makes the changes, then commits it.

Hope that helps!