Search code examples
gitsshhookgit-push

git pushes incorrect commit SHA when working via SSH


Background: OS: Windows Git version: 2.11.0

According to our policy, we are: 1. Adding several field to the commit messages of unpushed commits before push 2. Verifying on remote that pushed commit's message contains the required fields

We are editing the commit message via "git --filter-branch --msg-filter" which triggered during the pre-push hook.

When trying to push via SSH, git pushes the commit SHA before it was re-written. for example:

  1. Unpushed commit SHA is: 38dad1575a3c4239c967564c21347aad3d5b2a55
  2. Run git push
  3. Pre-push runs and re-writes the commit.
  4. Now my commit SHA is: 8115fdfb3be86a6b51284cd1d278bd55017990ce.
    Previous commit 38dad1575a3c4239c967564c21347aad3d5b2a55 is now stored under /refs/original/refs
  5. Push fails to the repo since commit 38dad1575a3c4239c967564c21347aad3d5b2a55(unedited commit) does not contain the necessary fields in its commit message.

This behavior occurs only when working over SSH. For users working over HTTPS everything works fine.

Any assistance will be appreciated.

Thanks!


Solution

  • As a sort of general rule, hooks are not allowed to modify commits themselves. There are specific cases that are explicitly allowed, and some that work by chance, but once a commit actually exists it cannot be changed. A git filter-branch or git commit --amend does not actually change any commit; instead, it adds a new commit that resembles the original, but has whatever change you had it make. This new commit has a new, different hash ID.

    What's surprising is not that this failed with ssh but that this succeeded with https. That's just luck, and one could argue that it is bad luck that it worked. A pre-push hook is only intended to say "yes, this push is allowed" or "no, this push is forbidden". It is not supposed to change a name-to-commit-ID mapping. Clearly what's actually happening is that the http based push winds up repeating the name to ID mapping and getting the new ID, while the ssh based push sticks with the ID it got initially.

    Now, you can actually make all this work:

    • In the pre-push hook, check whether the push is allowed.
      • If yes, pass it through.
      • If no, prepare to reject it. Make a new commit that would be allowed, and optionally, push that (recursively, with an inner git push running during the outer git push—the recursion will terminate since the new commit just made would be allowed, so that this branch of the logic will not run during the inner push). Then print an error message, noting that (and why) the original push was rejected and whether an "inner" push was done and if so whether it succeeded, or if not, what its ID is and how the user can use it. Finally, reject the push.

    There are several reasons to avoid this recursive method: (a) it violates the expectations of any experienced Git user, who would not expect his own commit to be replaced and pushed; and (b) if it goes wrong—if the recursion does not terminate immediately, for instance—it may go very wrong. Part (b) can be mitigated by cleverness in the pre-push hook (e.g., export an environment variable to the inner push noting that recursion is occurring, and if about to recurse, fail noisily if this variable is set). Part (a) is perhaps excusable given that the pre-push hook is, after all, something the user must set up himself in the first place.

    Even if you handle these objections, there's one more reason to avoid it: it makes the outer (non-recursive) push fail. The user will see that the push failed, even if the inner push succeeded. This is going to be at least a little bit confusing.