statquant
statquant

Reputation: 14400

How can I force commit from prod user to have --author <the_real_author>

At my workplace we use git for version control under Linux (centos 7 flavour). We have user ids but also have a few prod ids that we each can impersonate.

Because we're weak we've allowed the prod ids to commit and push in repos, typically for practicality when we are hot fixing an issue. We are supposed to use --author when doing it but we forget from time to time.

Is there a way to enforce the use of --author when using specific prod users ?

Upvotes: 4

Views: 1000

Answers (1)

torek
torek

Reputation: 489858

Once a commit is made, it can never be changed. But you do have the option to not send a commit, and another option: refuse to accept a commit. Thus, there are exactly three options here:

  • Always make the commit correctly, by doing something at the point where the commit is going to be made that checks to make sure it's being made correctly, and does not proceed if not. For instance, instead of making the commit using the command git commit, write your own program. Or, write a pre-commit hook and do the checking there, and never bypass the hook.

  • Check before sending. If you're using git push to send commits from Git Repository A to Git Repository B, the sending Git (Git A) will run its pre-push hook after phoning up Git B, but before transferring commits to Git B. At this time, you can, in the pre-push hook, inspect the commits: if they're not the form you like, don't send them.

  • Check before receiving, in a pre-receive or update hook. This requires action on the server itself. Since you mentioned Bitbucket, see their page on server-side hooks. A pre-receive hook can reject a git push (the whole thing); an update hook can reject one portion of a git push (one name-update).

The client-side hooks—pre-commit and pre-push—have the advantage that they're under your control. This is also their disadvantage: every person who uses git commit or git push must install the hooks into their own repositories, which gives them a chance to forget to install it; and they can bypass the hooks on purpose, or uninstall them.

The server-side hook has the advantage that no client—i.e., none of your users—can bypass it, either accidentally or intentionally. The server enforces it. Again, this is also its disadvantage. Enforcement is entirely up to the server, so unless you control the server—or whoever does control the server delegates this authority to you—you can't even install a hook in the first place.

Since I don't use Bitbucket, I can't really go into detail on how to use Bitbucket's client-side control over server-side hooks, but if you read through the page I linked, and follow more links, you should be able to read up on how to use what they offer.

Writing a pre-commit hook

A pre-commit hook is relatively simple: Git will look inside .git/hooks/ for a file named pre-commit. If this file exists and is executable, git commit will run it. This file is just a program, written in any language you like: Python, Perl, sh/bash, C, C++, Java, Go, whatever. If you write in an interpreted language, be sure that the OS's exec system call will invoke the interpreter. That is, for a /bin/sh script, make the first line of the file read #! /bin/sh, so that an exec system call can run it directly, and then chmod +x the file.

Your program's job is to inspect what is about to be committed—including, e.g., command-line options, if you can somehow figure out what they were—and exit with a zero (success) status if the commit should be allowed, or a nonzero (e.g., 1) exit status if the commit should be denied.

For your particular case, you are in some degree of luck: the hook is run after Git sets up the GIT_AUTHOR_NAME and other Git environment variables (this is based on testing with Git 2.21; be aware that the default CentOS Git may be ancient, so check your Git version and verify that this works in your version, or upgrade if needed). For instance, here's a somewhat silly pre-commit hook I wrote just to test this out:

$ cat .git/hooks/pre-commit
#! /bin/sh
env | grep GIT | sed 's/@/ /'
exit 1

This pre-commit hook simply denies every commit, after showing all the environment variables that have the string GIT in them (and removing @ to cut down on spam after I post this). So now (after chmod +x):

$ git commit
GIT_EXEC_PATH=/usr/local/libexec/git-core
GIT_INDEX_FILE=.git/index
GIT_AUTHOR_NAME=Chris Torek
GIT_PREFIX=
GIT_AUTHOR_EMAIL=chris.torek gmail.com
GIT_AUTHOR_DATE= 1570904668 -0700

Or:

$ git commit --author='A U Thor <[email protected]>'
GIT_EXEC_PATH=/usr/local/libexec/git-core
GIT_INDEX_FILE=.git/index
GIT_AUTHOR_NAME=A U Thor
GIT_PREFIX=
GIT_AUTHOR_EMAIL=thor example.com
GIT_AUTHOR_DATE= 1570904927 -0700

It should now be obvious how to check the "author" setting. You can choose to do it always, or only if id says that you're running as a "prod user" (whatever that means in practice).

Writing a pre-push hook

A pre-push hook is similar, but it happens after the commit or commits exist. Note that you cannot change the commits—it's too late to do that; the user has to throw out any bad commits in favor of new-and-improved replacements—but you can check commits that your Git is about to send.

As before, your hook can be written in any language you like, though shell scripts are generally going to be the easiest here. Its job is to inspect the already-existing commits, verify that you're willing to send them to the receiving Git, and exit 0 if so. If the pre-push hook exits nonzero, the entire push gets aborted.

Rather than command-line arguments, a pre-push hook receives its input on its standard input, one line at a time. So your hook must read all input lines. Each line has four strings in it, separated by one space each, and terminated by a line-feed (no carriage-return):

<local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF

(as noted in the githooks documentation).

Unfortunately, at the time of the git push, the remote sha-1 hash ID may or may not be in the local repository. If it's not, the only thing you know about it is that if both you and the other Git allow the push to complete, some commit(s) will be lost from the remote ref. So for this case, you might want to abort the push, since it will only succeed if the user used --force and git push --force is often undesirable. (If the user really needs to force this push, he/she/they/whatever can use git push --n-verify -f to bypass the pre-push hook too ... or, they could run git fetch first, so that the commit exists locally.)

After verifying that the remote's hash ID exists locally, so that you can inspect outgoing commits (and/or commits to be dropped), you can use git rev-list to obtain the hash IDs of such commits.

As a completely untested example, consider this bit of shell script:

while read localref localhash remoteref remotehash; do
    case $remoteref in
    refs/heads/*)
        remotebranch=${localref#refs/heads/};;
    *)
        echo "pushing to non-branch $remoteref - not checking this one"
        continue;;
    esac
    if ! git rev-parse --quiet --verify $remotehash >/dev/null; then
        echo "push to branch $remotebranch aborted"
        echo "need commits from them; please run git fetch first"
        exit 1
    fi
    git rev-list $remotehash..$remotehash | while read hash; do
        if ! check-commit $hash; then
            echo "git push to branch $remotebranch aborted: commit $hash failed check"
            exit 1
        fi
    done
    n_removed=$(git rev-list --count $localhash..$remotehash)
    if [ $n_removed -ne 0 ]; then
        echo "warning: force push discards $n_removed commits from $remotebranch"
    fi
done

If this shell script fragment is correct (remember, it's untested), it still requires that you write your own shell function check-commit. This function should inspect the commit that is going to be sent, and make sure that you are willing to send it to the other Git.

Writing a server-side pre-receive hook

If you have total control over the server Git, you can write your own pre-receive hook. As before it is just an executable program, written in any language you like, that the OS can invoke via its exec system call, stored in a file named .git/hooks/pre-receive.

As with the pre-push hook above, a pre-receive hook gets its input as a series of lines on its standard input, one per requested reference update. Your job is to read all the lines, verify that all the incoming commits are allowed, and if so, exit zero. If some commits should be rejected, you simply exit nonzero. It's a very good idea to print out why you're rejecting the commits, so that whoever is running git push sees your output, prefixed with the word remote:, so that they can tell why their push is rejected. But just exiting nonzero will cause the entire push to be rejected.

Writing a server-side update hook

If you have total control over the server Git, you can write your own update hook. As before it is just an executable program, written in any language you like, that the OS can invoke via its exec system call, stored in a file named .git/hooks/update.

The update hook receives its arguments as actual arguments. The order of the arguments differs from the order of lines fed to a pre-receive hook. Consult the githooks documentation for details. As with a pre-receive hook, your job is to inspect the proposed reference update, and exit 0 to permit the update, or nonzero to reject it. Unlike the pre-receive hook, rejecting one update does not automatically reject the others. Like the pre-receive hook, it's a very good idea to print a message about why the update is rejected.

Upvotes: 6

Related Questions