GIT - the stupid content tracker

– Linus Torvalds, Initial revision of “git”, the information manager from hell

After years of code review with stacked diffs1, I’ve been using GitLab merge requests at work.

Merge requests frustrated me until helpful folks pointed me toward GerritLab, a small Python tool for making stacked merge requests in GitLab—exactly what I was looking for.

But to talk to GitLab, GerritLab required a cleartext token in my ~/.gitconfig. I wanted to stow my token in a password vault, so I crafted a change for GerritLab that used gitcredentials(7).

Like most git features, git credentials are obscure, byzantine, and incredibly useful. It works like this:

import subprocess, json

INPUT = """\
protocol=https
host=example.com
username=thcipriani

"""

git_credentials_fill = subprocess.run(
    ["git", "credential", "fill"],
    input=INPUT,
    text=True,
    stdout=subprocess.PIPE,
)

git_credentials = {
    key: value for line in git_credentials_fill.stdout.splitlines()
    if '=' in line
    for key, value in [line.split('=', 1)]
}

print(json.dumps(git_credentials, indent=4))

Which looks like this when you run it:

$ ./example-git-creds.py
Password for 'https://thcipriani@example.com':
{
    "protocol": "https",
    "host": "example.com",
    "username": "thcipriani",
    "password": "hunter2"
}

The magic here is the shell command git credentials fill, which:

  1. Accepts a protocol, username, and host on standard input.
  2. Delegates to a “git credential helper” (git-credential-libsecret in my case). A credential helper is an executable that retrieves passwords from the OS or another program that provides secure storage.
  3. My git credential helper checks for credentials matching https://thcipriani@example.com and finds none.
  4. Since my credential helper comes up empty, git prompts me for my password.
  5. Git sends <key>=<value>\n pairs to standard output for each of the keys protocol, host, username, and password.

To stow the password for later, I can use git credential approve.

subprocess.run(
    ["git", "credential", "approve"],
    input=git_credentials_fill.stdout,
    text=True
)

If I do that, the next time I run the script, git finds the password without prompting:

$ ./example-git-creds.py
{
    "protocol": "https",
    "host": "example.com",
    "username": "thcipriani",
    "password": "hunter2"
}

Git credential’s purpose

The problem git credentials solve is this:

  • With git over ssh, you use your keys.
  • With git over https, you type a password. Over and over and over.

Beleaguered git maintainers solved this dilemma with the credential storage system—git credentials.

With the right configuration, git will stop asking for your password when you push to an https remote.

Instead, git credentials retrieve and send auth info to remotes.

The maze of options

My mind initially refused to learn git credentials due to its twisty little maze of terms that all sound alike:

  • git credential fill: how you invoke a user’s configured git credential helper
  • git credential approve: how you save git credentials (if this is supported by the user’s git credential helper)
  • git credential.helper: the git config that points to a script that poops out usernames and passwords. These helper scripts are often named git-credential-<something>.
  • git-credential-cache: a specific, built-in git credential helper that caches credentials in memory for a while.
  • git-credential-store: STOP. DON’T TOUCH. This is a specific, built-in git credential helper that stores credentials in cleartext in your home directory. Whomp whomp.
  • git-credential-manager: a specific and confusingly named git credential helper from Microsoft®. If you’re on Linux or Mac, feel free to ignore it.

But once I mapped the terms, I only needed to pick a git credential helper.

Configuring good credential helpers

The built-in git-credential-store is a bad credential helper—it saves your passwords in cleartext in ~/.git-credentials.2

If you’re on a Mac, you’re in luck3—one command points git credentials to your keychain:

git config --global credential.helper osxkeychain

Third-party developers have contributed helpers for popular password stores:

Meanwhile, Linux and Windows have standard options. Git’s source repo includes helpers for these options in the contrib directory.

On Linux, you can use libsecret. Here’s how I configured it on Debian:

sudo apt install libsecret-1-0 libsecret-1-dev
cd /usr/share/doc/git/contrib/credential/libsecret/
sudo make
sudo mv git-credential-libsecret /usr/local/bin/
git config --global credential.helper libsecret

On Windows, you can use the confusingly named git credential manager. I have no idea how to do this, and I refuse to learn.

Now, if you clone a repo over https, you can push over https without pain4. Plus, now you have a handy password library for shell scripts:

#!/usr/bin/env bash

input="\
protocol=https
host=example.com
user=thcipriani

"
eval "$(echo "$input" | git credential fill)"

echo "The password is: $password"

  1. stacked diffs” or “stacked pull-requests”—there’s no universal term.↩︎

  2. git-credential-store is not a git credential helper of honor. No highly-esteemed passwords should be stored with it. This message is a warning about danger. The danger is still present, in your time, as it was in ours.↩︎

  3. I think. I only have Linux computers to test this on, sorry ;_;↩︎

  4. Or the config option pushInsteadOf, which is what I actually do.↩︎