Implement git-credential-himitsu v1 PROPOSED

Hugo Osvaldo Barrera: 1
 Implement git-credential-himitsu

 3 files changed, 116 insertions(+), 0 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/himitsu-devel/patches/34025/mbox | git am -3
Learn more about email & git

[PATCH] Implement git-credential-himitsu Export this patch

Hi! This patch is not in a "good to merge" state. I'm mostly looking for
feedback at this stage.

I'm not sure if there's a well-defined method to import modules from another
package yet, so I just hacked this in-repo for now.

The naming of the helper is so that git will pick it up by simply using:

    	helper = himitsu

I haven't implemented `set` and `erase` yet. These are for saving password into
himitsu when they are provided to git manually or via some other helper. I
haven't found that workflow very intuitive, but will likely implement those
commands just for completeness's sake.

Something annoying is that git uses proto=smtp for smtp+tls. Other tools use
proto=smtps. I worry that other tools might try to use the password save as
proto=smtp for plain-text-smtp. But this can only be fixed by git itself, and
would likely be an unpopular change. git SHOULD pass the port to make the
protocol more unambiguous.

Looking forward to hearing some feedback.


This helper can be used by git to retrieve the smtps password. Example
usage is:

    	helper = himitsu
    	annotate = yes
    	confirm = always
    	smtpserver = smtp.fastmail.com
    	smtpuser = whynothugo@fastmail.com
    	smtpencryption = tls
    	smtpserverport = 587

The username MUST be specified in the git configuration, otherwise git
will not use the credentials helper.

This can be tested with:

    hare build -o git-credential-himitsu cmd/git-credential-himitsu/ && \
      cat test-input | ./git-credential-himitsu get

Where test-input should contain:

 .gitignore                         |   1 +
 Makefile                           |   4 ++
 cmd/git-credential-himitsu/main.ha | 111 +++++++++++++++++++++++++++++
 3 files changed, 116 insertions(+)
 create mode 100644 cmd/git-credential-himitsu/main.ha

diff --git a/.gitignore b/.gitignore
index ed61257..5cc6416 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
diff --git a/Makefile b/Makefile
index a61630f..86b2d03 100644
--- a/Makefile
+++ b/Makefile
@@ -23,6 +23,10 @@ himitsu-init:
	hare build -o $@ cmd/$@/

	hare build -o $@ cmd/$@/

	hare test

diff --git a/cmd/git-credential-himitsu/main.ha b/cmd/git-credential-himitsu/main.ha
new file mode 100644
index 0000000..fa4ba1c
--- /dev/null
+++ b/cmd/git-credential-himitsu/main.ha
@@ -0,0 +1,111 @@
use bufio;
use dirs;
use errors;
use getopt;
use fmt;
use himitsu::client;
use himitsu::query;
use io;
use net::unix;
use net;
use os;
use path;
use strings;

export fn main() void = {
	// Verbs are `get`, `store` and `erase`.
	// See: https://git-scm.com/docs/gitcredentials#_custom_helpers
	if (len(os::args) != 2) {
		fmt::fatal("No command specified");
	switch (os::args[1]) {
	case "get" =>
	case "set" =>
		fmt::fatalf("Only `get` is implemented");
	case "erase" =>
		fmt::fatalf("Only `get` is implemented");
	case =>
		fmt::fatalf("Usage: {} get", os::args[0]);

fn get() void = {
	let query: []str = [];

	for (true) {
		match (bufio::scanline(os::stdin)!) {
		case io::EOF =>
		case let line: []u8 =>
			const line = strings::fromutf8(line);
			defer free(line);
			const split = strings::split(line, "=");
			defer free(split);

			const param = line_to_query(split[0], split[1], query);
			append(query, param);

	// TODO: handle error:
	let himitsu = client::connect()!;
	defer io::close(himitsu)!;

	match (get_password(himitsu, query)) {
	case let password: str =>
		fmt::printfln("password={}", password)!;
	case void =>
		fmt::fatal("No matching credentials found.");

// For a given input line, return a corresponding portion of a query.
fn line_to_query(key: str, value: str, query: []str) str = {
	switch (key) {
	case "protocol" =>
		// FIXME: git sends "smtp" for proto "smtps".
		// Other tools use `smtps` for `smtps`, so entries need to be duplicated?
		return fmt::asprintf("proto={}", value);
	case "username" =>
		return fmt::asprintf("username={}", value);
	case "host" =>
		const (hostname, port) = strings::cut(value, ":");
		return fmt::asprintf("hostname={}", hostname);
	case =>
		// XXX: Should warn?
		return "";

// Get the password from a store.
fn get_password(conn: net::socket, query: []str) (str | void) = {
	fmt::fprintf(conn, "query -d ")!;
	for (let i = 0z; i < len(query); i += 1) {
		fmt::fprintf(conn, "{} ", query[i])!;
	fmt::fprintf(conn, "\n")!;

	match (bufio::scanline(conn)!) {
	case io::EOF =>
		return; // How do I no-op here?
	case let line: []u8 =>
		const line = strings::fromutf8(line);
		defer free(line);

		if (line == "end") {

		const components = strings::split(line, " ");
		defer free(components);

		for (let i = 0z; i < len(components); i += 1) {
			const (key, value) = strings::cut(components[i], "=");

			if (key == "password!") {
				return value;
Only comment is that this should not live in the himitsu repository but
in a separate himitsu-git repo.