about summary refs log tree commit diff
path: root/contrib/credential
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/credential')
-rw-r--r--contrib/credential/gnome-keyring/.gitignore1
-rw-r--r--contrib/credential/gnome-keyring/Makefile25
-rw-r--r--contrib/credential/gnome-keyring/git-credential-gnome-keyring.c470
-rw-r--r--contrib/credential/libsecret/Makefile25
-rw-r--r--contrib/credential/libsecret/git-credential-libsecret.c369
-rw-r--r--contrib/credential/netrc/Makefile8
-rwxr-xr-xcontrib/credential/netrc/git-credential-netrc440
-rwxr-xr-xcontrib/credential/netrc/t-git-credential-netrc.sh32
-rwxr-xr-xcontrib/credential/netrc/test.command-option-gpg2
-rwxr-xr-xcontrib/credential/netrc/test.git-config-gpg2
-rw-r--r--contrib/credential/netrc/test.netrc13
-rw-r--r--contrib/credential/netrc/test.netrc.gpg0
-rwxr-xr-xcontrib/credential/netrc/test.pl139
-rw-r--r--contrib/credential/osxkeychain/.gitignore1
-rw-r--r--contrib/credential/osxkeychain/Makefile17
-rw-r--r--contrib/credential/osxkeychain/git-credential-osxkeychain.c183
-rw-r--r--contrib/credential/wincred/Makefile22
-rw-r--r--contrib/credential/wincred/git-credential-wincred.c327
18 files changed, 2076 insertions, 0 deletions
diff --git a/contrib/credential/gnome-keyring/.gitignore b/contrib/credential/gnome-keyring/.gitignore
new file mode 100644
index 000000000000..88d8fcdbce7d
--- /dev/null
+++ b/contrib/credential/gnome-keyring/.gitignore
@@ -0,0 +1 @@
+git-credential-gnome-keyring
diff --git a/contrib/credential/gnome-keyring/Makefile b/contrib/credential/gnome-keyring/Makefile
new file mode 100644
index 000000000000..22c19df94b81
--- /dev/null
+++ b/contrib/credential/gnome-keyring/Makefile
@@ -0,0 +1,25 @@
+MAIN:=git-credential-gnome-keyring
+all:: $(MAIN)
+
+CC = gcc
+RM = rm -f
+CFLAGS = -g -O2 -Wall
+PKG_CONFIG = pkg-config
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+INCS:=$(shell $(PKG_CONFIG) --cflags gnome-keyring-1 glib-2.0)
+LIBS:=$(shell $(PKG_CONFIG) --libs gnome-keyring-1 glib-2.0)
+
+SRCS:=$(MAIN).c
+OBJS:=$(SRCS:.c=.o)
+
+%.o: %.c
+	$(CC) $(CFLAGS) $(CPPFLAGS) $(INCS) -o $@ -c $<
+
+$(MAIN): $(OBJS)
+	$(CC) -o $@ $(LDFLAGS) $^ $(LIBS)
+
+clean:
+	@$(RM) $(MAIN) $(OBJS)
diff --git a/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c
new file mode 100644
index 000000000000..d389bfadceeb
--- /dev/null
+++ b/contrib/credential/gnome-keyring/git-credential-gnome-keyring.c
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2011 John Szakmeister <john@szakmeister.net>
+ *               2012 Philipp A. Hartmann <pah@qo.cx>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Credits:
+ * - GNOME Keyring API handling originally written by John Szakmeister
+ * - ported to credential helper API by Philipp A. Hartmann
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <glib.h>
+#include <gnome-keyring.h>
+
+#ifdef GNOME_KEYRING_DEFAULT
+
+   /* Modern gnome-keyring */
+
+#include <gnome-keyring-memory.h>
+
+#else
+
+   /*
+    * Support ancient gnome-keyring, circ. RHEL 5.X.
+    * GNOME_KEYRING_DEFAULT seems to have been introduced with Gnome 2.22,
+    * and the other features roughly around Gnome 2.20, 6 months before.
+    * Ubuntu 8.04 used Gnome 2.22 (I think).  Not sure any distro used 2.20.
+    * So the existence/non-existence of GNOME_KEYRING_DEFAULT seems like
+    * a decent thing to use as an indicator.
+    */
+
+#define GNOME_KEYRING_DEFAULT NULL
+
+/*
+ * ancient gnome-keyring returns DENIED when an entry is not found.
+ * Setting NO_MATCH to DENIED will prevent us from reporting DENIED
+ * errors during get and erase operations, but we will still report
+ * DENIED errors during a store.
+ */
+#define GNOME_KEYRING_RESULT_NO_MATCH GNOME_KEYRING_RESULT_DENIED
+
+#define gnome_keyring_memory_alloc g_malloc
+#define gnome_keyring_memory_free gnome_keyring_free_password
+#define gnome_keyring_memory_strdup g_strdup
+
+static const char *gnome_keyring_result_to_message(GnomeKeyringResult result)
+{
+	switch (result) {
+	case GNOME_KEYRING_RESULT_OK:
+		return "OK";
+	case GNOME_KEYRING_RESULT_DENIED:
+		return "Denied";
+	case GNOME_KEYRING_RESULT_NO_KEYRING_DAEMON:
+		return "No Keyring Daemon";
+	case GNOME_KEYRING_RESULT_ALREADY_UNLOCKED:
+		return "Already UnLocked";
+	case GNOME_KEYRING_RESULT_NO_SUCH_KEYRING:
+		return "No Such Keyring";
+	case GNOME_KEYRING_RESULT_BAD_ARGUMENTS:
+		return "Bad Arguments";
+	case GNOME_KEYRING_RESULT_IO_ERROR:
+		return "IO Error";
+	case GNOME_KEYRING_RESULT_CANCELLED:
+		return "Cancelled";
+	case GNOME_KEYRING_RESULT_ALREADY_EXISTS:
+		return "Already Exists";
+	default:
+		return "Unknown Error";
+	}
+}
+
+/*
+ * Support really ancient gnome-keyring, circ. RHEL 4.X.
+ * Just a guess for the Glib version.  Glib 2.8 was roughly Gnome 2.12 ?
+ * Which was released with gnome-keyring 0.4.3 ??
+ */
+#if GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION < 8
+
+static void gnome_keyring_done_cb(GnomeKeyringResult result, gpointer user_data)
+{
+	gpointer *data = (gpointer *)user_data;
+	int *done = (int *)data[0];
+	GnomeKeyringResult *r = (GnomeKeyringResult *)data[1];
+
+	*r = result;
+	*done = 1;
+}
+
+static void wait_for_request_completion(int *done)
+{
+	GMainContext *mc = g_main_context_default();
+	while (!*done)
+		g_main_context_iteration(mc, TRUE);
+}
+
+static GnomeKeyringResult gnome_keyring_item_delete_sync(const char *keyring, guint32 id)
+{
+	int done = 0;
+	GnomeKeyringResult result;
+	gpointer data[] = { &done, &result };
+
+	gnome_keyring_item_delete(keyring, id, gnome_keyring_done_cb, data,
+		NULL);
+
+	wait_for_request_completion(&done);
+
+	return result;
+}
+
+#endif
+#endif
+
+/*
+ * This credential struct and API is simplified from git's credential.{h,c}
+ */
+struct credential {
+	char *protocol;
+	char *host;
+	unsigned short port;
+	char *path;
+	char *username;
+	char *password;
+};
+
+#define CREDENTIAL_INIT { NULL, NULL, 0, NULL, NULL, NULL }
+
+typedef int (*credential_op_cb)(struct credential *);
+
+struct credential_operation {
+	char *name;
+	credential_op_cb op;
+};
+
+#define CREDENTIAL_OP_END { NULL, NULL }
+
+/* ----------------- GNOME Keyring functions ----------------- */
+
+/* create a special keyring option string, if path is given */
+static char *keyring_object(struct credential *c)
+{
+	if (!c->path)
+		return NULL;
+
+	if (c->port)
+		return g_strdup_printf("%s:%hd/%s", c->host, c->port, c->path);
+
+	return g_strdup_printf("%s/%s", c->host, c->path);
+}
+
+static int keyring_get(struct credential *c)
+{
+	char *object = NULL;
+	GList *entries;
+	GnomeKeyringNetworkPasswordData *password_data;
+	GnomeKeyringResult result;
+
+	if (!c->protocol || !(c->host || c->path))
+		return EXIT_FAILURE;
+
+	object = keyring_object(c);
+
+	result = gnome_keyring_find_network_password_sync(
+				c->username,
+				NULL /* domain */,
+				c->host,
+				object,
+				c->protocol,
+				NULL /* authtype */,
+				c->port,
+				&entries);
+
+	g_free(object);
+
+	if (result == GNOME_KEYRING_RESULT_NO_MATCH)
+		return EXIT_SUCCESS;
+
+	if (result == GNOME_KEYRING_RESULT_CANCELLED)
+		return EXIT_SUCCESS;
+
+	if (result != GNOME_KEYRING_RESULT_OK) {
+		g_critical("%s", gnome_keyring_result_to_message(result));
+		return EXIT_FAILURE;
+	}
+
+	/* pick the first one from the list */
+	password_data = (GnomeKeyringNetworkPasswordData *)entries->data;
+
+	gnome_keyring_memory_free(c->password);
+	c->password = gnome_keyring_memory_strdup(password_data->password);
+
+	if (!c->username)
+		c->username = g_strdup(password_data->user);
+
+	gnome_keyring_network_password_list_free(entries);
+
+	return EXIT_SUCCESS;
+}
+
+
+static int keyring_store(struct credential *c)
+{
+	guint32 item_id;
+	char *object = NULL;
+	GnomeKeyringResult result;
+
+	/*
+	 * Sanity check that what we are storing is actually sensible.
+	 * In particular, we can't make a URL without a protocol field.
+	 * Without either a host or pathname (depending on the scheme),
+	 * we have no primary key. And without a username and password,
+	 * we are not actually storing a credential.
+	 */
+	if (!c->protocol || !(c->host || c->path) ||
+	    !c->username || !c->password)
+		return EXIT_FAILURE;
+
+	object = keyring_object(c);
+
+	result = gnome_keyring_set_network_password_sync(
+				GNOME_KEYRING_DEFAULT,
+				c->username,
+				NULL /* domain */,
+				c->host,
+				object,
+				c->protocol,
+				NULL /* authtype */,
+				c->port,
+				c->password,
+				&item_id);
+
+	g_free(object);
+
+	if (result != GNOME_KEYRING_RESULT_OK &&
+	    result != GNOME_KEYRING_RESULT_CANCELLED) {
+		g_critical("%s", gnome_keyring_result_to_message(result));
+		return EXIT_FAILURE;
+	}
+
+	return EXIT_SUCCESS;
+}
+
+static int keyring_erase(struct credential *c)
+{
+	char *object = NULL;
+	GList *entries;
+	GnomeKeyringNetworkPasswordData *password_data;
+	GnomeKeyringResult result;
+
+	/*
+	 * Sanity check that we actually have something to match
+	 * against. The input we get is a restrictive pattern,
+	 * so technically a blank credential means "erase everything".
+	 * But it is too easy to accidentally send this, since it is equivalent
+	 * to empty input. So explicitly disallow it, and require that the
+	 * pattern have some actual content to match.
+	 */
+	if (!c->protocol && !c->host && !c->path && !c->username)
+		return EXIT_FAILURE;
+
+	object = keyring_object(c);
+
+	result = gnome_keyring_find_network_password_sync(
+				c->username,
+				NULL /* domain */,
+				c->host,
+				object,
+				c->protocol,
+				NULL /* authtype */,
+				c->port,
+				&entries);
+
+	g_free(object);
+
+	if (result == GNOME_KEYRING_RESULT_NO_MATCH)
+		return EXIT_SUCCESS;
+
+	if (result == GNOME_KEYRING_RESULT_CANCELLED)
+		return EXIT_SUCCESS;
+
+	if (result != GNOME_KEYRING_RESULT_OK) {
+		g_critical("%s", gnome_keyring_result_to_message(result));
+		return EXIT_FAILURE;
+	}
+
+	/* pick the first one from the list (delete all matches?) */
+	password_data = (GnomeKeyringNetworkPasswordData *)entries->data;
+
+	result = gnome_keyring_item_delete_sync(
+		password_data->keyring, password_data->item_id);
+
+	gnome_keyring_network_password_list_free(entries);
+
+	if (result != GNOME_KEYRING_RESULT_OK) {
+		g_critical("%s", gnome_keyring_result_to_message(result));
+		return EXIT_FAILURE;
+	}
+
+	return EXIT_SUCCESS;
+}
+
+/*
+ * Table with helper operation callbacks, used by generic
+ * credential helper main function.
+ */
+static struct credential_operation const credential_helper_ops[] = {
+	{ "get",   keyring_get },
+	{ "store", keyring_store },
+	{ "erase", keyring_erase },
+	CREDENTIAL_OP_END
+};
+
+/* ------------------ credential functions ------------------ */
+
+static void credential_init(struct credential *c)
+{
+	memset(c, 0, sizeof(*c));
+}
+
+static void credential_clear(struct credential *c)
+{
+	g_free(c->protocol);
+	g_free(c->host);
+	g_free(c->path);
+	g_free(c->username);
+	gnome_keyring_memory_free(c->password);
+
+	credential_init(c);
+}
+
+static int credential_read(struct credential *c)
+{
+	char *buf;
+	size_t line_len;
+	char *key;
+	char *value;
+
+	key = buf = gnome_keyring_memory_alloc(1024);
+
+	while (fgets(buf, 1024, stdin)) {
+		line_len = strlen(buf);
+
+		if (line_len && buf[line_len-1] == '\n')
+			buf[--line_len] = '\0';
+
+		if (!line_len)
+			break;
+
+		value = strchr(buf, '=');
+		if (!value) {
+			g_warning("invalid credential line: %s", key);
+			gnome_keyring_memory_free(buf);
+			return -1;
+		}
+		*value++ = '\0';
+
+		if (!strcmp(key, "protocol")) {
+			g_free(c->protocol);
+			c->protocol = g_strdup(value);
+		} else if (!strcmp(key, "host")) {
+			g_free(c->host);
+			c->host = g_strdup(value);
+			value = strrchr(c->host, ':');
+			if (value) {
+				*value++ = '\0';
+				c->port = atoi(value);
+			}
+		} else if (!strcmp(key, "path")) {
+			g_free(c->path);
+			c->path = g_strdup(value);
+		} else if (!strcmp(key, "username")) {
+			g_free(c->username);
+			c->username = g_strdup(value);
+		} else if (!strcmp(key, "password")) {
+			gnome_keyring_memory_free(c->password);
+			c->password = gnome_keyring_memory_strdup(value);
+			while (*value)
+				*value++ = '\0';
+		}
+		/*
+		 * Ignore other lines; we don't know what they mean, but
+		 * this future-proofs us when later versions of git do
+		 * learn new lines, and the helpers are updated to match.
+		 */
+	}
+
+	gnome_keyring_memory_free(buf);
+
+	return 0;
+}
+
+static void credential_write_item(FILE *fp, const char *key, const char *value)
+{
+	if (!value)
+		return;
+	fprintf(fp, "%s=%s\n", key, value);
+}
+
+static void credential_write(const struct credential *c)
+{
+	/* only write username/password, if set */
+	credential_write_item(stdout, "username", c->username);
+	credential_write_item(stdout, "password", c->password);
+}
+
+static void usage(const char *name)
+{
+	struct credential_operation const *try_op = credential_helper_ops;
+	const char *basename = strrchr(name, '/');
+
+	basename = (basename) ? basename + 1 : name;
+	fprintf(stderr, "usage: %s <", basename);
+	while (try_op->name) {
+		fprintf(stderr, "%s", (try_op++)->name);
+		if (try_op->name)
+			fprintf(stderr, "%s", "|");
+	}
+	fprintf(stderr, "%s", ">\n");
+}
+
+int main(int argc, char *argv[])
+{
+	int ret = EXIT_SUCCESS;
+
+	struct credential_operation const *try_op = credential_helper_ops;
+	struct credential cred = CREDENTIAL_INIT;
+
+	if (!argv[1]) {
+		usage(argv[0]);
+		exit(EXIT_FAILURE);
+	}
+
+	g_set_application_name("Git Credential Helper");
+
+	/* lookup operation callback */
+	while (try_op->name && strcmp(argv[1], try_op->name))
+		try_op++;
+
+	/* unsupported operation given -- ignore silently */
+	if (!try_op->name || !try_op->op)
+		goto out;
+
+	ret = credential_read(&cred);
+	if (ret)
+		goto out;
+
+	/* perform credential operation */
+	ret = (*try_op->op)(&cred);
+
+	credential_write(&cred);
+
+out:
+	credential_clear(&cred);
+	return ret;
+}
diff --git a/contrib/credential/libsecret/Makefile b/contrib/credential/libsecret/Makefile
new file mode 100644
index 000000000000..3e67552cc5b5
--- /dev/null
+++ b/contrib/credential/libsecret/Makefile
@@ -0,0 +1,25 @@
+MAIN:=git-credential-libsecret
+all:: $(MAIN)
+
+CC = gcc
+RM = rm -f
+CFLAGS = -g -O2 -Wall
+PKG_CONFIG = pkg-config
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+INCS:=$(shell $(PKG_CONFIG) --cflags libsecret-1 glib-2.0)
+LIBS:=$(shell $(PKG_CONFIG) --libs libsecret-1 glib-2.0)
+
+SRCS:=$(MAIN).c
+OBJS:=$(SRCS:.c=.o)
+
+%.o: %.c
+	$(CC) $(CFLAGS) $(CPPFLAGS) $(INCS) -o $@ -c $<
+
+$(MAIN): $(OBJS)
+	$(CC) -o $@ $(LDFLAGS) $^ $(LIBS)
+
+clean:
+	@$(RM) $(MAIN) $(OBJS)
diff --git a/contrib/credential/libsecret/git-credential-libsecret.c b/contrib/credential/libsecret/git-credential-libsecret.c
new file mode 100644
index 000000000000..e6598b638339
--- /dev/null
+++ b/contrib/credential/libsecret/git-credential-libsecret.c
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2011 John Szakmeister <john@szakmeister.net>
+ *               2012 Philipp A. Hartmann <pah@qo.cx>
+ *               2016 Mantas Mikulėnas <grawity@gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Credits:
+ * - GNOME Keyring API handling originally written by John Szakmeister
+ * - ported to credential helper API by Philipp A. Hartmann
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <glib.h>
+#include <libsecret/secret.h>
+
+/*
+ * This credential struct and API is simplified from git's credential.{h,c}
+ */
+struct credential {
+	char *protocol;
+	char *host;
+	unsigned short port;
+	char *path;
+	char *username;
+	char *password;
+};
+
+#define CREDENTIAL_INIT { NULL, NULL, 0, NULL, NULL, NULL }
+
+typedef int (*credential_op_cb)(struct credential *);
+
+struct credential_operation {
+	char *name;
+	credential_op_cb op;
+};
+
+#define CREDENTIAL_OP_END { NULL, NULL }
+
+/* ----------------- Secret Service functions ----------------- */
+
+static char *make_label(struct credential *c)
+{
+	if (c->port)
+		return g_strdup_printf("Git: %s://%s:%hu/%s",
+					c->protocol, c->host, c->port, c->path ? c->path : "");
+	else
+		return g_strdup_printf("Git: %s://%s/%s",
+					c->protocol, c->host, c->path ? c->path : "");
+}
+
+static GHashTable *make_attr_list(struct credential *c)
+{
+	GHashTable *al = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
+
+	if (c->username)
+		g_hash_table_insert(al, "user", g_strdup(c->username));
+	if (c->protocol)
+		g_hash_table_insert(al, "protocol", g_strdup(c->protocol));
+	if (c->host)
+		g_hash_table_insert(al, "server", g_strdup(c->host));
+	if (c->port)
+		g_hash_table_insert(al, "port", g_strdup_printf("%hu", c->port));
+	if (c->path)
+		g_hash_table_insert(al, "object", g_strdup(c->path));
+
+	return al;
+}
+
+static int keyring_get(struct credential *c)
+{
+	SecretService *service = NULL;
+	GHashTable *attributes = NULL;
+	GError *error = NULL;
+	GList *items = NULL;
+
+	if (!c->protocol || !(c->host || c->path))
+		return EXIT_FAILURE;
+
+	service = secret_service_get_sync(0, NULL, &error);
+	if (error != NULL) {
+		g_critical("could not connect to Secret Service: %s", error->message);
+		g_error_free(error);
+		return EXIT_FAILURE;
+	}
+
+	attributes = make_attr_list(c);
+	items = secret_service_search_sync(service,
+					   SECRET_SCHEMA_COMPAT_NETWORK,
+					   attributes,
+					   SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_UNLOCK,
+					   NULL,
+					   &error);
+	g_hash_table_unref(attributes);
+	if (error != NULL) {
+		g_critical("lookup failed: %s", error->message);
+		g_error_free(error);
+		return EXIT_FAILURE;
+	}
+
+	if (items != NULL) {
+		SecretItem *item;
+		SecretValue *secret;
+		const char *s;
+
+		item = items->data;
+		secret = secret_item_get_secret(item);
+		attributes = secret_item_get_attributes(item);
+
+		s = g_hash_table_lookup(attributes, "user");
+		if (s) {
+			g_free(c->username);
+			c->username = g_strdup(s);
+		}
+
+		s = secret_value_get_text(secret);
+		if (s) {
+			g_free(c->password);
+			c->password = g_strdup(s);
+		}
+
+		g_hash_table_unref(attributes);
+		secret_value_unref(secret);
+		g_list_free_full(items, g_object_unref);
+	}
+
+	return EXIT_SUCCESS;
+}
+
+
+static int keyring_store(struct credential *c)
+{
+	char *label = NULL;
+	GHashTable *attributes = NULL;
+	GError *error = NULL;
+
+	/*
+	 * Sanity check that what we are storing is actually sensible.
+	 * In particular, we can't make a URL without a protocol field.
+	 * Without either a host or pathname (depending on the scheme),
+	 * we have no primary key. And without a username and password,
+	 * we are not actually storing a credential.
+	 */
+	if (!c->protocol || !(c->host || c->path) ||
+	    !c->username || !c->password)
+		return EXIT_FAILURE;
+
+	label = make_label(c);
+	attributes = make_attr_list(c);
+	secret_password_storev_sync(SECRET_SCHEMA_COMPAT_NETWORK,
+				    attributes,
+				    NULL,
+				    label,
+				    c->password,
+				    NULL,
+				    &error);
+	g_free(label);
+	g_hash_table_unref(attributes);
+
+	if (error != NULL) {
+		g_critical("store failed: %s", error->message);
+		g_error_free(error);
+		return EXIT_FAILURE;
+	}
+
+	return EXIT_SUCCESS;
+}
+
+static int keyring_erase(struct credential *c)
+{
+	GHashTable *attributes = NULL;
+	GError *error = NULL;
+
+	/*
+	 * Sanity check that we actually have something to match
+	 * against. The input we get is a restrictive pattern,
+	 * so technically a blank credential means "erase everything".
+	 * But it is too easy to accidentally send this, since it is equivalent
+	 * to empty input. So explicitly disallow it, and require that the
+	 * pattern have some actual content to match.
+	 */
+	if (!c->protocol && !c->host && !c->path && !c->username)
+		return EXIT_FAILURE;
+
+	attributes = make_attr_list(c);
+	secret_password_clearv_sync(SECRET_SCHEMA_COMPAT_NETWORK,
+				    attributes,
+				    NULL,
+				    &error);
+	g_hash_table_unref(attributes);
+
+	if (error != NULL) {
+		g_critical("erase failed: %s", error->message);
+		g_error_free(error);
+		return EXIT_FAILURE;
+	}
+
+	return EXIT_SUCCESS;
+}
+
+/*
+ * Table with helper operation callbacks, used by generic
+ * credential helper main function.
+ */
+static struct credential_operation const credential_helper_ops[] = {
+	{ "get",   keyring_get },
+	{ "store", keyring_store },
+	{ "erase", keyring_erase },
+	CREDENTIAL_OP_END
+};
+
+/* ------------------ credential functions ------------------ */
+
+static void credential_init(struct credential *c)
+{
+	memset(c, 0, sizeof(*c));
+}
+
+static void credential_clear(struct credential *c)
+{
+	g_free(c->protocol);
+	g_free(c->host);
+	g_free(c->path);
+	g_free(c->username);
+	g_free(c->password);
+
+	credential_init(c);
+}
+
+static int credential_read(struct credential *c)
+{
+	char *buf;
+	size_t line_len;
+	char *key;
+	char *value;
+
+	key = buf = g_malloc(1024);
+
+	while (fgets(buf, 1024, stdin)) {
+		line_len = strlen(buf);
+
+		if (line_len && buf[line_len-1] == '\n')
+			buf[--line_len] = '\0';
+
+		if (!line_len)
+			break;
+
+		value = strchr(buf, '=');
+		if (!value) {
+			g_warning("invalid credential line: %s", key);
+			g_free(buf);
+			return -1;
+		}
+		*value++ = '\0';
+
+		if (!strcmp(key, "protocol")) {
+			g_free(c->protocol);
+			c->protocol = g_strdup(value);
+		} else if (!strcmp(key, "host")) {
+			g_free(c->host);
+			c->host = g_strdup(value);
+			value = strrchr(c->host, ':');
+			if (value) {
+				*value++ = '\0';
+				c->port = atoi(value);
+			}
+		} else if (!strcmp(key, "path")) {
+			g_free(c->path);
+			c->path = g_strdup(value);
+		} else if (!strcmp(key, "username")) {
+			g_free(c->username);
+			c->username = g_strdup(value);
+		} else if (!strcmp(key, "password")) {
+			g_free(c->password);
+			c->password = g_strdup(value);
+			while (*value)
+				*value++ = '\0';
+		}
+		/*
+		 * Ignore other lines; we don't know what they mean, but
+		 * this future-proofs us when later versions of git do
+		 * learn new lines, and the helpers are updated to match.
+		 */
+	}
+
+	g_free(buf);
+
+	return 0;
+}
+
+static void credential_write_item(FILE *fp, const char *key, const char *value)
+{
+	if (!value)
+		return;
+	fprintf(fp, "%s=%s\n", key, value);
+}
+
+static void credential_write(const struct credential *c)
+{
+	/* only write username/password, if set */
+	credential_write_item(stdout, "username", c->username);
+	credential_write_item(stdout, "password", c->password);
+}
+
+static void usage(const char *name)
+{
+	struct credential_operation const *try_op = credential_helper_ops;
+	const char *basename = strrchr(name, '/');
+
+	basename = (basename) ? basename + 1 : name;
+	fprintf(stderr, "usage: %s <", basename);
+	while (try_op->name) {
+		fprintf(stderr, "%s", (try_op++)->name);
+		if (try_op->name)
+			fprintf(stderr, "%s", "|");
+	}
+	fprintf(stderr, "%s", ">\n");
+}
+
+int main(int argc, char *argv[])
+{
+	int ret = EXIT_SUCCESS;
+
+	struct credential_operation const *try_op = credential_helper_ops;
+	struct credential cred = CREDENTIAL_INIT;
+
+	if (!argv[1]) {
+		usage(argv[0]);
+		exit(EXIT_FAILURE);
+	}
+
+	g_set_application_name("Git Credential Helper");
+
+	/* lookup operation callback */
+	while (try_op->name && strcmp(argv[1], try_op->name))
+		try_op++;
+
+	/* unsupported operation given -- ignore silently */
+	if (!try_op->name || !try_op->op)
+		goto out;
+
+	ret = credential_read(&cred);
+	if (ret)
+		goto out;
+
+	/* perform credential operation */
+	ret = (*try_op->op)(&cred);
+
+	credential_write(&cred);
+
+out:
+	credential_clear(&cred);
+	return ret;
+}
diff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile
new file mode 100644
index 000000000000..6174e3bb8382
--- /dev/null
+++ b/contrib/credential/netrc/Makefile
@@ -0,0 +1,8 @@
+# The default target of this Makefile is...
+all::
+
+test:
+	./t-git-credential-netrc.sh
+
+testverbose:
+	./t-git-credential-netrc.sh -d -v
diff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc
new file mode 100755
index 000000000000..ebfc123ec641
--- /dev/null
+++ b/contrib/credential/netrc/git-credential-netrc
@@ -0,0 +1,440 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+use File::Basename;
+use Git;
+
+my $VERSION = "0.2";
+
+my %options = (
+	       help => 0,
+	       debug => 0,
+	       verbose => 0,
+	       insecure => 0,
+	       file => [],
+
+	       # identical token maps, e.g. host -> host, will be inserted later
+	       tmap => {
+			port => 'protocol',
+			machine => 'host',
+			path => 'path',
+			login => 'username',
+			user => 'username',
+			password => 'password',
+		       }
+	      );
+
+# Map each credential protocol token to itself on the netrc side.
+foreach (values %{$options{tmap}}) {
+	$options{tmap}->{$_} = $_;
+}
+
+# Now, $options{tmap} has a mapping from the netrc format to the Git credential
+# helper protocol.
+
+# Next, we build the reverse token map.
+
+# When $rmap{foo} contains 'bar', that means that what the Git credential helper
+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in
+# %rmap are what we expect to read from the netrc/authinfo file.
+
+my %rmap;
+foreach my $k (keys %{$options{tmap}}) {
+	push @{$rmap{$options{tmap}->{$k}}}, $k;
+}
+
+Getopt::Long::Configure("bundling");
+
+# TODO: maybe allow the token map $options{tmap} to be configurable.
+GetOptions(\%options,
+           "help|h",
+           "debug|d",
+           "insecure|k",
+           "verbose|v",
+           "file|f=s@",
+           'gpg|g:s',
+          );
+
+if ($options{help}) {
+	my $shortname = basename($0);
+	$shortname =~ s/git-credential-//;
+
+	print <<EOHIPPUS;
+
+$0 [(-f <authfile>)...] [-g <program>] [-d] [-v] [-k] get
+
+Version $VERSION by tzz\@lifelogs.com.  License: BSD.
+
+Options:
+
+  -f|--file <authfile>: specify netrc-style files.  Files with the .gpg
+                        extension will be decrypted by GPG before parsing.
+                        Multiple -f arguments are OK.  They are processed in
+                        order, and the first matching entry found is returned
+                        via the credential helper protocol (see below).
+
+                        When no -f option is given, .authinfo.gpg, .netrc.gpg,
+                        .authinfo, and .netrc files in your home directory are
+                        used in this order.
+
+  -g|--gpg <program>  : specify the program for GPG. By default, this is the
+                        value of gpg.program in the git repository or global
+                        option or gpg.
+
+  -k|--insecure       : ignore bad file ownership or permissions
+
+  -d|--debug          : turn on debugging (developer info)
+
+  -v|--verbose        : be more verbose (show files and information found)
+
+To enable this credential helper:
+
+  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'
+
+(Note that Git will prepend "git-credential-" to the helper name and look for it
+in the path.)
+
+...and if you want lots of debugging info:
+
+  git config credential.helper '$shortname -f AUTHFILE -d'
+
+...or to see the files opened and data found:
+
+  git config credential.helper '$shortname -f AUTHFILE -v'
+
+Only "get" mode is supported by this credential helper.  It opens every
+<authfile> and looks for the first entry that matches the requested search
+criteria:
+
+ 'port|protocol':
+   The protocol that will be used (e.g., https). (protocol=X)
+
+ 'machine|host':
+   The remote hostname for a network credential. (host=X)
+
+ 'path':
+   The path with which the credential will be used. (path=X)
+
+ 'login|user|username':
+   The credential’s username, if we already have one. (username=X)
+
+Thus, when we get this query on STDIN:
+
+host=github.com
+protocol=https
+username=tzz
+
+this credential helper will look for the first entry in every <authfile> that
+matches
+
+machine github.com port https login tzz
+
+OR
+
+machine github.com protocol https login tzz
+
+OR... etc. acceptable tokens as listed above.  Any unknown tokens are
+simply ignored.
+
+Then, the helper will print out whatever tokens it got from the entry, including
+"password" tokens, mapping back to Git's helper protocol; e.g. "port" is mapped
+back to "protocol".  Any redundant entry tokens (part of the original query) are
+skipped.
+
+Again, note that only the first matching entry from all the <authfile>s,
+processed in the sequence given on the command line, is used.
+
+Netrc/authinfo tokens can be quoted as 'STRING' or "STRING".
+
+No caching is performed by this credential helper.
+
+EOHIPPUS
+
+	exit 0;
+}
+
+my $mode = shift @ARGV;
+
+# Credentials must get a parameter, so die if it's missing.
+die "Syntax: $0 [(-f <authfile>)...] [-d] get" unless defined $mode;
+
+# Only support 'get' mode; with any other unsupported ones we just exit.
+exit 0 unless $mode eq 'get';
+
+my $files = $options{file};
+
+# if no files were given, use a predefined list.
+# note that .gpg files come first
+unless (scalar @$files) {
+	my @candidates = qw[
+				   ~/.authinfo.gpg
+				   ~/.netrc.gpg
+				   ~/.authinfo
+				   ~/.netrc
+			  ];
+
+	$files = $options{file} = [ map { glob $_ } @candidates ];
+}
+
+load_config(\%options);
+
+my $query = read_credential_data_from_stdin();
+
+FILE:
+foreach my $file (@$files) {
+	my $gpgmode = $file =~ m/\.gpg$/;
+	unless (-r $file) {
+		log_verbose("Unable to read $file; skipping it");
+		next FILE;
+	}
+
+	# the following check is copied from Net::Netrc, for non-GPG files
+	# OS/2 and Win32 do not handle stat in a way compatible with this check :-(
+	unless ($gpgmode || $options{insecure} ||
+		$^O eq 'os2'
+		|| $^O eq 'MSWin32'
+		|| $^O eq 'MacOS'
+		|| $^O =~ /^cygwin/) {
+		my @stat = stat($file);
+
+		if (@stat) {
+			if ($stat[2] & 077) {
+				log_verbose("Insecure $file (mode=%04o); skipping it",
+					    $stat[2] & 07777);
+				next FILE;
+			}
+
+			if ($stat[4] != $<) {
+				log_verbose("Not owner of $file; skipping it");
+				next FILE;
+			}
+		}
+	}
+
+	my @entries = load_netrc($file, $gpgmode);
+
+	unless (scalar @entries) {
+		if ($!) {
+			log_verbose("Unable to open $file: $!");
+		} else {
+			log_verbose("No netrc entries found in $file");
+		}
+
+		next FILE;
+	}
+
+	my $entry = find_netrc_entry($query, @entries);
+	if ($entry) {
+		print_credential_data($entry, $query);
+		# we're done!
+		last FILE;
+	}
+}
+
+exit 0;
+
+sub load_netrc {
+	my $file = shift @_;
+	my $gpgmode = shift @_;
+
+	my $io;
+	if ($gpgmode) {
+		my @cmd = ($options{'gpg'}, qw(--decrypt), $file);
+		log_verbose("Using GPG to open $file: [@cmd]");
+		open $io, "-|", @cmd;
+	} else {
+		log_verbose("Opening $file...");
+		open $io, '<', $file;
+	}
+
+	# nothing to do if the open failed (we log the error later)
+	return unless $io;
+
+	# Net::Netrc does this, but the functionality is merged with the file
+	# detection logic, so we have to extract just the part we need
+	my @netrc_entries = net_netrc_loader($io);
+
+	# these entries will use the credential helper protocol token names
+	my @entries;
+
+	foreach my $nentry (@netrc_entries) {
+		my %entry;
+		my $num_port;
+
+		if (!defined $nentry->{machine}) {
+			next;
+		}
+		if (defined $nentry->{port} && $nentry->{port} =~ m/^\d+$/) {
+			$num_port = $nentry->{port};
+			delete $nentry->{port};
+		}
+
+		# create the new entry for the credential helper protocol
+		$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;
+
+		# for "host X port Y" where Y is an integer (captured by
+		# $num_port above), set the host to "X:Y"
+		if (defined $entry{host} && defined $num_port) {
+			$entry{host} = join(':', $entry{host}, $num_port);
+		}
+
+		push @entries, \%entry;
+	}
+
+	return @entries;
+}
+
+sub net_netrc_loader {
+	my $fh = shift @_;
+	my @entries;
+	my ($mach, $macdef, $tok, @tok);
+
+    LINE:
+	while (<$fh>) {
+		undef $macdef if /\A\n\Z/;
+
+		if ($macdef) {
+			next LINE;
+		}
+
+		s/^\s*//;
+		chomp;
+
+		while (length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*//) {
+			(my $tok = $+) =~ s/\\(.)/$1/g;
+			push(@tok, $tok);
+		}
+
+	    TOKEN:
+		while (@tok) {
+			if ($tok[0] eq "default") {
+				shift(@tok);
+				$mach = { machine => undef };
+				next TOKEN;
+			}
+
+			$tok = shift(@tok);
+
+			if ($tok eq "machine") {
+				my $host = shift @tok;
+				$mach = { machine => $host };
+				push @entries, $mach;
+			} elsif (exists $options{tmap}->{$tok}) {
+				unless ($mach) {
+					log_debug("Skipping token $tok because no machine was given");
+					next TOKEN;
+				}
+
+				my $value = shift @tok;
+				unless (defined $value) {
+					log_debug("Token $tok had no value, skipping it.");
+					next TOKEN;
+				}
+
+				# Following line added by rmerrell to remove '/' escape char in .netrc
+				$value =~ s/\/\\/\\/g;
+				$mach->{$tok} = $value;
+			} elsif ($tok eq "macdef") { # we ignore macros
+				next TOKEN unless $mach;
+				my $value = shift @tok;
+				$macdef = 1;
+			}
+		}
+	}
+
+	return @entries;
+}
+
+sub read_credential_data_from_stdin {
+	# the query: start with every token with no value
+	my %q = map { $_ => undef } values(%{$options{tmap}});
+
+	while (<STDIN>) {
+		next unless m/^([^=]+)=(.+)/;
+
+		my ($token, $value) = ($1, $2);
+		die "Unknown search token $token" unless exists $q{$token};
+		$q{$token} = $value;
+		log_debug("We were given search token $token and value $value");
+	}
+
+	foreach (sort keys %q) {
+		log_debug("Searching for %s = %s", $_, $q{$_} || '(any value)');
+	}
+
+	return \%q;
+}
+
+# takes the search tokens and then a list of entries
+# each entry is a hash reference
+sub find_netrc_entry {
+	my $query = shift @_;
+
+    ENTRY:
+	foreach my $entry (@_)
+	{
+		my $entry_text = join ', ', map { "$_=$entry->{$_}" } keys %$entry;
+		foreach my $check (sort keys %$query) {
+			if (!defined $entry->{$check}) {
+			        log_debug("OK: entry has no $check token, so any value satisfies check $check");
+			} elsif (defined $query->{$check}) {
+				log_debug("compare %s [%s] to [%s] (entry: %s)",
+					  $check,
+					  $entry->{$check},
+					  $query->{$check},
+					  $entry_text);
+				unless ($query->{$check} eq $entry->{$check}) {
+					next ENTRY;
+				}
+			} else {
+				log_debug("OK: any value satisfies check $check");
+			}
+		}
+
+		return $entry;
+	}
+
+	# nothing was found
+	return;
+}
+
+sub print_credential_data {
+	my $entry = shift @_;
+	my $query = shift @_;
+
+	log_debug("entry has passed all the search checks");
+ TOKEN:
+	foreach my $git_token (sort keys %$entry) {
+		log_debug("looking for useful token $git_token");
+		# don't print unknown (to the credential helper protocol) tokens
+		next TOKEN unless exists $query->{$git_token};
+
+		# don't print things asked in the query (the entry matches them)
+		next TOKEN if defined $query->{$git_token};
+
+		log_debug("FOUND: $git_token=$entry->{$git_token}");
+		printf "%s=%s\n", $git_token, $entry->{$git_token};
+	}
+}
+sub load_config {
+	# load settings from git config
+	my $options = shift;
+	# set from command argument, gpg.program option, or default to gpg
+	$options->{'gpg'} //= Git->repository()->config('gpg.program')
+	                  // 'gpg';
+	log_verbose("using $options{'gpg'} for GPG operations");
+}
+sub log_verbose {
+	return unless $options{verbose};
+	printf STDERR @_;
+	printf STDERR "\n";
+}
+
+sub log_debug {
+	return unless $options{debug};
+	printf STDERR @_;
+	printf STDERR "\n";
+}
diff --git a/contrib/credential/netrc/t-git-credential-netrc.sh b/contrib/credential/netrc/t-git-credential-netrc.sh
new file mode 100755
index 000000000000..07227d022876
--- /dev/null
+++ b/contrib/credential/netrc/t-git-credential-netrc.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+(
+	cd ../../../t
+	test_description='git-credential-netrc'
+	. ./test-lib.sh
+
+	if ! test_have_prereq PERL; then
+		skip_all='skipping perl interface tests, perl not available'
+		test_done
+	fi
+
+	perl -MTest::More -e 0 2>/dev/null || {
+		skip_all="Perl Test::More unavailable, skipping test"
+		test_done
+	}
+
+	# set up test repository
+
+	test_expect_success \
+		'set up test repository' \
+		'git config --add gpg.program test.git-config-gpg'
+
+	# The external test will outputs its own plan
+	test_external_has_tap=1
+
+	export PERL5LIB="$GITPERLLIB"
+	test_external \
+		'git-credential-netrc' \
+		perl "$GIT_BUILD_DIR"/contrib/credential/netrc/test.pl
+
+	test_done
+)
diff --git a/contrib/credential/netrc/test.command-option-gpg b/contrib/credential/netrc/test.command-option-gpg
new file mode 100755
index 000000000000..d8f1285d416d
--- /dev/null
+++ b/contrib/credential/netrc/test.command-option-gpg
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo machine command-option-gpg login username password password
diff --git a/contrib/credential/netrc/test.git-config-gpg b/contrib/credential/netrc/test.git-config-gpg
new file mode 100755
index 000000000000..65cf594c2001
--- /dev/null
+++ b/contrib/credential/netrc/test.git-config-gpg
@@ -0,0 +1,2 @@
+#!/bin/sh
+echo machine git-config-gpg login username password password
diff --git a/contrib/credential/netrc/test.netrc b/contrib/credential/netrc/test.netrc
new file mode 100644
index 000000000000..ba119a937f67
--- /dev/null
+++ b/contrib/credential/netrc/test.netrc
@@ -0,0 +1,13 @@
+machine imap login tzz@lifelogs.com port imaps password letmeknow
+machine imap login bob port imaps password bobwillknow
+
+# comment test
+
+machine imap2 login tzz port 1099 password tzzknow
+machine imap2 login bob password bobwillknow
+
+# another command
+
+machine github.com
+  multilinetoken anothervalue
+  login carol password carolknows
diff --git a/contrib/credential/netrc/test.netrc.gpg b/contrib/credential/netrc/test.netrc.gpg
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/contrib/credential/netrc/test.netrc.gpg
diff --git a/contrib/credential/netrc/test.pl b/contrib/credential/netrc/test.pl
new file mode 100755
index 000000000000..c0fb3718b280
--- /dev/null
+++ b/contrib/credential/netrc/test.pl
@@ -0,0 +1,139 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Test::More qw(no_plan);
+use File::Basename;
+use File::Spec::Functions qw(:DEFAULT rel2abs);
+use IPC::Open2;
+
+BEGIN {
+	# t-git-credential-netrc.sh kicks off our testing, so we have to go
+	# from there.
+	Test::More->builder->current_test(1);
+}
+
+my @global_credential_args = @ARGV;
+my $scriptDir = dirname rel2abs $0;
+my ($netrc, $netrcGpg, $gcNetrc) = map { catfile $scriptDir, $_; }
+                                       qw(test.netrc
+                                          test.netrc.gpg
+                                          git-credential-netrc);
+local $ENV{PATH} = join ':'
+                      , $scriptDir
+                      , $ENV{PATH}
+                      ? $ENV{PATH}
+                      : ();
+
+diag "Testing insecure file, nothing should be found\n";
+chmod 0644, $netrc;
+my $cred = run_credential(['-f', $netrc, 'get'],
+			  { host => 'github.com' });
+
+ok(scalar keys %$cred == 0, "Got 0 keys from insecure file");
+
+diag "Testing missing file, nothing should be found\n";
+chmod 0644, $netrc;
+$cred = run_credential(['-f', '///nosuchfile///', 'get'],
+		       { host => 'github.com' });
+
+ok(scalar keys %$cred == 0, "Got 0 keys from missing file");
+
+chmod 0600, $netrc;
+
+diag "Testing with invalid data\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       "bad data");
+ok(scalar keys %$cred == 4, "Got first found keys with bad data");
+
+diag "Testing netrc file for a missing corovamilkbar entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       { host => 'corovamilkbar' });
+
+ok(scalar keys %$cred == 0, "Got no corovamilkbar keys");
+
+diag "Testing netrc file for a github.com entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       { host => 'github.com' });
+
+ok(scalar keys %$cred == 2, "Got 2 Github keys");
+
+is($cred->{password}, 'carolknows', "Got correct Github password");
+is($cred->{username}, 'carol', "Got correct Github username");
+
+diag "Testing netrc file for a username-specific entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       { host => 'imap', username => 'bob' });
+
+ok(scalar keys %$cred == 2, "Got 2 username-specific keys");
+
+is($cred->{password}, 'bobwillknow', "Got correct user-specific password");
+is($cred->{protocol}, 'imaps', "Got correct user-specific protocol");
+
+diag "Testing netrc file for a host:port-specific entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       { host => 'imap2:1099' });
+
+ok(scalar keys %$cred == 2, "Got 2 host:port-specific keys");
+
+is($cred->{password}, 'tzzknow', "Got correct host:port-specific password");
+is($cred->{username}, 'tzz', "Got correct host:port-specific username");
+
+diag "Testing netrc file that 'host:port kills host' entry\n";
+$cred = run_credential(['-f', $netrc, 'get'],
+		       { host => 'imap2' });
+
+ok(scalar keys %$cred == 2, "Got 2 'host:port kills host' keys");
+
+is($cred->{password}, 'bobwillknow', "Got correct 'host:port kills host' password");
+is($cred->{username}, 'bob', "Got correct 'host:port kills host' username");
+
+diag 'Testing netrc file decryption by git config gpg.program setting\n';
+$cred = run_credential( ['-f', $netrcGpg, 'get']
+                      , { host => 'git-config-gpg' }
+                      );
+
+ok(scalar keys %$cred == 2, 'Got keys decrypted by git config option');
+
+diag 'Testing netrc file decryption by gpg option\n';
+$cred = run_credential( ['-f', $netrcGpg, '-g', 'test.command-option-gpg', 'get']
+                      , { host => 'command-option-gpg' }
+                      );
+
+ok(scalar keys %$cred == 2, 'Got keys decrypted by command option');
+
+my $is_passing = eval { Test::More->is_passing };
+exit($is_passing ? 0 : 1) unless $@ =~ /Can't locate object method/;
+
+sub run_credential
+{
+	my $args = shift @_;
+	my $data = shift @_;
+	my $pid = open2(my $chld_out, my $chld_in,
+			$gcNetrc, @global_credential_args,
+			@$args);
+
+	die "Couldn't open pipe to netrc credential helper: $!" unless $pid;
+
+	if (ref $data eq 'HASH')
+	{
+		print $chld_in "$_=$data->{$_}\n" foreach sort keys %$data;
+	}
+	else
+	{
+		print $chld_in "$data\n";
+	}
+
+	close $chld_in;
+	my %ret;
+
+	while (<$chld_out>)
+	{
+		chomp;
+		next unless m/^([^=]+)=(.+)/;
+
+		$ret{$1} = $2;
+	}
+
+	return \%ret;
+}
diff --git a/contrib/credential/osxkeychain/.gitignore b/contrib/credential/osxkeychain/.gitignore
new file mode 100644
index 000000000000..6c5b7026c53d
--- /dev/null
+++ b/contrib/credential/osxkeychain/.gitignore
@@ -0,0 +1 @@
+git-credential-osxkeychain
diff --git a/contrib/credential/osxkeychain/Makefile b/contrib/credential/osxkeychain/Makefile
new file mode 100644
index 000000000000..4b3a08a2bac4
--- /dev/null
+++ b/contrib/credential/osxkeychain/Makefile
@@ -0,0 +1,17 @@
+all:: git-credential-osxkeychain
+
+CC = gcc
+RM = rm -f
+CFLAGS = -g -O2 -Wall
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+git-credential-osxkeychain: git-credential-osxkeychain.o
+	$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) -Wl,-framework -Wl,Security
+
+git-credential-osxkeychain.o: git-credential-osxkeychain.c
+	$(CC) -c $(CFLAGS) $<
+
+clean:
+	$(RM) git-credential-osxkeychain git-credential-osxkeychain.o
diff --git a/contrib/credential/osxkeychain/git-credential-osxkeychain.c b/contrib/credential/osxkeychain/git-credential-osxkeychain.c
new file mode 100644
index 000000000000..bcd3f575a3e3
--- /dev/null
+++ b/contrib/credential/osxkeychain/git-credential-osxkeychain.c
@@ -0,0 +1,183 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <Security/Security.h>
+
+static SecProtocolType protocol;
+static char *host;
+static char *path;
+static char *username;
+static char *password;
+static UInt16 port;
+
+static void die(const char *err, ...)
+{
+	char msg[4096];
+	va_list params;
+	va_start(params, err);
+	vsnprintf(msg, sizeof(msg), err, params);
+	fprintf(stderr, "%s\n", msg);
+	va_end(params);
+	exit(1);
+}
+
+static void *xstrdup(const char *s1)
+{
+	void *ret = strdup(s1);
+	if (!ret)
+		die("Out of memory");
+	return ret;
+}
+
+#define KEYCHAIN_ITEM(x) (x ? strlen(x) : 0), x
+#define KEYCHAIN_ARGS \
+	NULL, /* default keychain */ \
+	KEYCHAIN_ITEM(host), \
+	0, NULL, /* account domain */ \
+	KEYCHAIN_ITEM(username), \
+	KEYCHAIN_ITEM(path), \
+	port, \
+	protocol, \
+	kSecAuthenticationTypeDefault
+
+static void write_item(const char *what, const char *buf, int len)
+{
+	printf("%s=", what);
+	fwrite(buf, 1, len, stdout);
+	putchar('\n');
+}
+
+static void find_username_in_item(SecKeychainItemRef item)
+{
+	SecKeychainAttributeList list;
+	SecKeychainAttribute attr;
+
+	list.count = 1;
+	list.attr = &attr;
+	attr.tag = kSecAccountItemAttr;
+
+	if (SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL))
+		return;
+
+	write_item("username", attr.data, attr.length);
+	SecKeychainItemFreeContent(&list, NULL);
+}
+
+static void find_internet_password(void)
+{
+	void *buf;
+	UInt32 len;
+	SecKeychainItemRef item;
+
+	if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, &len, &buf, &item))
+		return;
+
+	write_item("password", buf, len);
+	if (!username)
+		find_username_in_item(item);
+
+	SecKeychainItemFreeContent(NULL, buf);
+}
+
+static void delete_internet_password(void)
+{
+	SecKeychainItemRef item;
+
+	/*
+	 * Require at least a protocol and host for removal, which is what git
+	 * will give us; if you want to do something more fancy, use the
+	 * Keychain manager.
+	 */
+	if (!protocol || !host)
+		return;
+
+	if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, 0, NULL, &item))
+		return;
+
+	SecKeychainItemDelete(item);
+}
+
+static void add_internet_password(void)
+{
+	/* Only store complete credentials */
+	if (!protocol || !host || !username || !password)
+		return;
+
+	if (SecKeychainAddInternetPassword(
+	      KEYCHAIN_ARGS,
+	      KEYCHAIN_ITEM(password),
+	      NULL))
+		return;
+}
+
+static void read_credential(void)
+{
+	char buf[1024];
+
+	while (fgets(buf, sizeof(buf), stdin)) {
+		char *v;
+
+		if (!strcmp(buf, "\n"))
+			break;
+		buf[strlen(buf)-1] = '\0';
+
+		v = strchr(buf, '=');
+		if (!v)
+			die("bad input: %s", buf);
+		*v++ = '\0';
+
+		if (!strcmp(buf, "protocol")) {
+			if (!strcmp(v, "imap"))
+				protocol = kSecProtocolTypeIMAP;
+			else if (!strcmp(v, "imaps"))
+				protocol = kSecProtocolTypeIMAPS;
+			else if (!strcmp(v, "ftp"))
+				protocol = kSecProtocolTypeFTP;
+			else if (!strcmp(v, "ftps"))
+				protocol = kSecProtocolTypeFTPS;
+			else if (!strcmp(v, "https"))
+				protocol = kSecProtocolTypeHTTPS;
+			else if (!strcmp(v, "http"))
+				protocol = kSecProtocolTypeHTTP;
+			else if (!strcmp(v, "smtp"))
+				protocol = kSecProtocolTypeSMTP;
+			else /* we don't yet handle other protocols */
+				exit(0);
+		}
+		else if (!strcmp(buf, "host")) {
+			char *colon = strchr(v, ':');
+			if (colon) {
+				*colon++ = '\0';
+				port = atoi(colon);
+			}
+			host = xstrdup(v);
+		}
+		else if (!strcmp(buf, "path"))
+			path = xstrdup(v);
+		else if (!strcmp(buf, "username"))
+			username = xstrdup(v);
+		else if (!strcmp(buf, "password"))
+			password = xstrdup(v);
+	}
+}
+
+int main(int argc, const char **argv)
+{
+	const char *usage =
+		"usage: git credential-osxkeychain <get|store|erase>";
+
+	if (!argv[1])
+		die(usage);
+
+	read_credential();
+
+	if (!strcmp(argv[1], "get"))
+		find_internet_password();
+	else if (!strcmp(argv[1], "store"))
+		add_internet_password();
+	else if (!strcmp(argv[1], "erase"))
+		delete_internet_password();
+	/* otherwise, ignore unknown action */
+
+	return 0;
+}
diff --git a/contrib/credential/wincred/Makefile b/contrib/credential/wincred/Makefile
new file mode 100644
index 000000000000..6e992c08667d
--- /dev/null
+++ b/contrib/credential/wincred/Makefile
@@ -0,0 +1,22 @@
+all: git-credential-wincred.exe
+
+-include ../../../config.mak.autogen
+-include ../../../config.mak
+
+CC ?= gcc
+RM ?= rm -f
+CFLAGS ?= -O2 -Wall
+
+prefix ?= /usr/local
+libexecdir ?= $(prefix)/libexec/git-core
+
+INSTALL ?= install
+
+git-credential-wincred.exe : git-credential-wincred.c
+	$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
+
+install: git-credential-wincred.exe
+	$(INSTALL) -m 755 $^ $(libexecdir)
+
+clean:
+	$(RM) git-credential-wincred.exe
diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c
new file mode 100644
index 000000000000..5bdad41de1f8
--- /dev/null
+++ b/contrib/credential/wincred/git-credential-wincred.c
@@ -0,0 +1,327 @@
+/*
+ * A git credential helper that interface with Windows' Credential Manager
+ *
+ */
+#include <windows.h>
+#include <stdio.h>
+#include <io.h>
+#include <fcntl.h>
+
+/* common helpers */
+
+#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))
+
+static void die(const char *err, ...)
+{
+	char msg[4096];
+	va_list params;
+	va_start(params, err);
+	vsnprintf(msg, sizeof(msg), err, params);
+	fprintf(stderr, "%s\n", msg);
+	va_end(params);
+	exit(1);
+}
+
+static void *xmalloc(size_t size)
+{
+	void *ret = malloc(size);
+	if (!ret && !size)
+		ret = malloc(1);
+	if (!ret)
+		 die("Out of memory");
+	return ret;
+}
+
+/* MinGW doesn't have wincred.h, so we need to define stuff */
+
+typedef struct _CREDENTIAL_ATTRIBUTEW {
+	LPWSTR Keyword;
+	DWORD  Flags;
+	DWORD  ValueSize;
+	LPBYTE Value;
+} CREDENTIAL_ATTRIBUTEW, *PCREDENTIAL_ATTRIBUTEW;
+
+typedef struct _CREDENTIALW {
+	DWORD                  Flags;
+	DWORD                  Type;
+	LPWSTR                 TargetName;
+	LPWSTR                 Comment;
+	FILETIME               LastWritten;
+	DWORD                  CredentialBlobSize;
+	LPBYTE                 CredentialBlob;
+	DWORD                  Persist;
+	DWORD                  AttributeCount;
+	PCREDENTIAL_ATTRIBUTEW Attributes;
+	LPWSTR                 TargetAlias;
+	LPWSTR                 UserName;
+} CREDENTIALW, *PCREDENTIALW;
+
+#define CRED_TYPE_GENERIC 1
+#define CRED_PERSIST_LOCAL_MACHINE 2
+#define CRED_MAX_ATTRIBUTES 64
+
+typedef BOOL (WINAPI *CredWriteWT)(PCREDENTIALW, DWORD);
+typedef BOOL (WINAPI *CredEnumerateWT)(LPCWSTR, DWORD, DWORD *,
+    PCREDENTIALW **);
+typedef VOID (WINAPI *CredFreeT)(PVOID);
+typedef BOOL (WINAPI *CredDeleteWT)(LPCWSTR, DWORD, DWORD);
+
+static HMODULE advapi;
+static CredWriteWT CredWriteW;
+static CredEnumerateWT CredEnumerateW;
+static CredFreeT CredFree;
+static CredDeleteWT CredDeleteW;
+
+static void load_cred_funcs(void)
+{
+	/* load DLLs */
+	advapi = LoadLibraryExA("advapi32.dll", NULL,
+				LOAD_LIBRARY_SEARCH_SYSTEM32);
+	if (!advapi)
+		die("failed to load advapi32.dll");
+
+	/* get function pointers */
+	CredWriteW = (CredWriteWT)GetProcAddress(advapi, "CredWriteW");
+	CredEnumerateW = (CredEnumerateWT)GetProcAddress(advapi,
+	    "CredEnumerateW");
+	CredFree = (CredFreeT)GetProcAddress(advapi, "CredFree");
+	CredDeleteW = (CredDeleteWT)GetProcAddress(advapi, "CredDeleteW");
+	if (!CredWriteW || !CredEnumerateW || !CredFree || !CredDeleteW)
+		die("failed to load functions");
+}
+
+static WCHAR *wusername, *password, *protocol, *host, *path, target[1024];
+
+static void write_item(const char *what, LPCWSTR wbuf, int wlen)
+{
+	char *buf;
+
+	if (!wbuf || !wlen) {
+		printf("%s=\n", what);
+		return;
+	}
+
+	int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, NULL, 0, NULL,
+	    FALSE);
+	buf = xmalloc(len);
+
+	if (!WideCharToMultiByte(CP_UTF8, 0, wbuf, wlen, buf, len, NULL, FALSE))
+		die("WideCharToMultiByte failed!");
+
+	printf("%s=", what);
+	fwrite(buf, 1, len, stdout);
+	putchar('\n');
+	free(buf);
+}
+
+/*
+ * Match an (optional) expected string and a delimiter in the target string,
+ * consuming the matched text by updating the target pointer.
+ */
+
+static LPCWSTR wcsstr_last(LPCWSTR str, LPCWSTR find)
+{
+	LPCWSTR res = NULL, pos;
+	for (pos = wcsstr(str, find); pos; pos = wcsstr(pos + 1, find))
+		res = pos;
+	return res;
+}
+
+static int match_part_with_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim, int last)
+{
+	LPCWSTR delim_pos, start = *ptarget;
+	int len;
+
+	/* find start of delimiter (or end-of-string if delim is empty) */
+	if (*delim)
+		delim_pos = last ? wcsstr_last(start, delim) : wcsstr(start, delim);
+	else
+		delim_pos = start + wcslen(start);
+
+	/*
+	 * match text up to delimiter, or end of string (e.g. the '/' after
+	 * host is optional if not followed by a path)
+	 */
+	if (delim_pos)
+		len = delim_pos - start;
+	else
+		len = wcslen(start);
+
+	/* update ptarget if we either found a delimiter or need a match */
+	if (delim_pos || want)
+		*ptarget = delim_pos ? delim_pos + wcslen(delim) : start + len;
+
+	return !want || (!wcsncmp(want, start, len) && !want[len]);
+}
+
+static int match_part(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
+{
+	return match_part_with_last(ptarget, want, delim, 0);
+}
+
+static int match_part_last(LPCWSTR *ptarget, LPCWSTR want, LPCWSTR delim)
+{
+	return match_part_with_last(ptarget, want, delim, 1);
+}
+
+static int match_cred(const CREDENTIALW *cred)
+{
+	LPCWSTR target = cred->TargetName;
+	if (wusername && wcscmp(wusername, cred->UserName ? cred->UserName : L""))
+		return 0;
+
+	return match_part(&target, L"git", L":") &&
+		match_part(&target, protocol, L"://") &&
+		match_part_last(&target, wusername, L"@") &&
+		match_part(&target, host, L"/") &&
+		match_part(&target, path, L"");
+}
+
+static void get_credential(void)
+{
+	CREDENTIALW **creds;
+	DWORD num_creds;
+	int i;
+
+	if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds))
+		return;
+
+	/* search for the first credential that matches username */
+	for (i = 0; i < num_creds; ++i)
+		if (match_cred(creds[i])) {
+			write_item("username", creds[i]->UserName,
+				creds[i]->UserName ? wcslen(creds[i]->UserName) : 0);
+			write_item("password",
+				(LPCWSTR)creds[i]->CredentialBlob,
+				creds[i]->CredentialBlobSize / sizeof(WCHAR));
+			break;
+		}
+
+	CredFree(creds);
+}
+
+static void store_credential(void)
+{
+	CREDENTIALW cred;
+
+	if (!wusername || !password)
+		return;
+
+	cred.Flags = 0;
+	cred.Type = CRED_TYPE_GENERIC;
+	cred.TargetName = target;
+	cred.Comment = L"saved by git-credential-wincred";
+	cred.CredentialBlobSize = (wcslen(password)) * sizeof(WCHAR);
+	cred.CredentialBlob = (LPVOID)password;
+	cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
+	cred.AttributeCount = 0;
+	cred.Attributes = NULL;
+	cred.TargetAlias = NULL;
+	cred.UserName = wusername;
+
+	if (!CredWriteW(&cred, 0))
+		die("CredWrite failed");
+}
+
+static void erase_credential(void)
+{
+	CREDENTIALW **creds;
+	DWORD num_creds;
+	int i;
+
+	if (!CredEnumerateW(L"git:*", 0, &num_creds, &creds))
+		return;
+
+	for (i = 0; i < num_creds; ++i) {
+		if (match_cred(creds[i]))
+			CredDeleteW(creds[i]->TargetName, creds[i]->Type, 0);
+	}
+
+	CredFree(creds);
+}
+
+static WCHAR *utf8_to_utf16_dup(const char *str)
+{
+	int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
+	WCHAR *wstr = xmalloc(sizeof(WCHAR) * wlen);
+	MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, wlen);
+	return wstr;
+}
+
+static void read_credential(void)
+{
+	char buf[1024];
+
+	while (fgets(buf, sizeof(buf), stdin)) {
+		char *v;
+		int len = strlen(buf);
+		/* strip trailing CR / LF */
+		while (len && strchr("\r\n", buf[len - 1]))
+			buf[--len] = 0;
+
+		if (!*buf)
+			break;
+
+		v = strchr(buf, '=');
+		if (!v)
+			die("bad input: %s", buf);
+		*v++ = '\0';
+
+		if (!strcmp(buf, "protocol"))
+			protocol = utf8_to_utf16_dup(v);
+		else if (!strcmp(buf, "host"))
+			host = utf8_to_utf16_dup(v);
+		else if (!strcmp(buf, "path"))
+			path = utf8_to_utf16_dup(v);
+		else if (!strcmp(buf, "username")) {
+			wusername = utf8_to_utf16_dup(v);
+		} else if (!strcmp(buf, "password"))
+			password = utf8_to_utf16_dup(v);
+		else
+			die("unrecognized input");
+	}
+}
+
+int main(int argc, char *argv[])
+{
+	const char *usage =
+	    "usage: git credential-wincred <get|store|erase>\n";
+
+	if (!argv[1])
+		die(usage);
+
+	/* git use binary pipes to avoid CRLF-issues */
+	_setmode(_fileno(stdin), _O_BINARY);
+	_setmode(_fileno(stdout), _O_BINARY);
+
+	read_credential();
+
+	load_cred_funcs();
+
+	if (!protocol || !(host || path))
+		return 0;
+
+	/* prepare 'target', the unique key for the credential */
+	wcscpy(target, L"git:");
+	wcsncat(target, protocol, ARRAY_SIZE(target));
+	wcsncat(target, L"://", ARRAY_SIZE(target));
+	if (wusername) {
+		wcsncat(target, wusername, ARRAY_SIZE(target));
+		wcsncat(target, L"@", ARRAY_SIZE(target));
+	}
+	if (host)
+		wcsncat(target, host, ARRAY_SIZE(target));
+	if (path) {
+		wcsncat(target, L"/", ARRAY_SIZE(target));
+		wcsncat(target, path, ARRAY_SIZE(target));
+	}
+
+	if (!strcmp(argv[1], "get"))
+		get_credential();
+	else if (!strcmp(argv[1], "store"))
+		store_credential();
+	else if (!strcmp(argv[1], "erase"))
+		erase_credential();
+	/* otherwise, ignore unknown action */
+	return 0;
+}