{"thread":{"id":"32825","subject":"[PATCH] Add contrib/credentials/netrc with GPG support","startedAt":"2013-02-04T19:54:30Z","lastAt":"2013-02-25T16:24:02Z","messageCount":38,"participants":["Ted Zlatanov","Jeff King","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"208639","messageId":"87ehgvua6h.fsf@lifelogs.com","threadId":"32825","inReplyTo":null,"subject":"[PATCH] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T19:54:30Z","receivedAt":"2013-02-04T19:54:30Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\n contrib/credential/netrc/git-credential-netrc |  223 +++++++++++++++++++++++++\n 1 files changed, 223 insertions(+), 0 deletions(-)\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..7b43aa9\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,223 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+               help => 0,\n+               debug => 0,\n+\n+               # identical token maps, e.g. host -> host, will be inserted later\n+               tmap => {\n+                        port => 'protocol',\n+                        machine => 'host',\n+                        path => 'path',\n+                        login => 'username',\n+                        user => 'username',\n+                        password => 'password',\n+                       }\n+              );\n+\n+# map each credential protocol token to itself on the netrc side\n+$options{tmap}->{$_} = $_ foreach my $v (values %{$options{tmap}});\n+\n+foreach my $suffix ('.gpg', '') {\n+\tforeach my $base (qw/authinfo netrc/) {\n+\t\tmy $file = glob(\"~/.$base$suffix\");\n+\t\tnext unless (defined $file && -f $file);\n+\t\t$options{file} = $file ;\n+\t}\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"file|f=s\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE] [-d] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+  -f AUTHFILE: specify a netrc-style file\n+  -d: turn on debugging\n+\n+To enable (note that Git will prepend \"git-credential-\" to the helper\n+name and look for it in the path):\n+\n+  git config credential.helper '$shortname -f AUTHFILE'\n+\n+And if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens\n+AUTHFILE and looks for entries that match the requested search\n+criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get \"protocol=https\\nusername=tzz\", this credential\n+helper will look for lines in AUTHFILE that match\n+\n+port https login tzz\n+\n+OR\n+\n+protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the line,\n+including \"password\" tokens, mapping e.g. \"port\" back to \"protocol\".\n+\n+The first matching line is used.  Tokens can be quoted as 'STRING' or\n+\"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# credentials may get 'get', 'store', or 'erase' as parameters but\n+# only acknowledge 'get'\n+die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n+\n+# only support 'get' mode\n+exit unless $mode eq 'get';\n+\n+my $debug = $options{debug};\n+my $file = $options{file};\n+\n+die \"Sorry, you need to specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\"\n+ unless defined $file;\n+\n+unless (-f $file) {\n+\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n+\texit 0;\n+}\n+\n+my @data;\n+if ($file =~ m/\\.gpg$/) {\n+\t@data = load('-|', qw(gpg --decrypt), $file)\n+}\n+else {\n+\t@data = load('<', $file);\n+}\n+\n+chomp @data;\n+\n+unless (scalar @data) {\n+\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n+\texit;\n+}\n+\n+# the query: start with every token with no value\n+my %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+while (<STDIN>) {\n+\tnext unless m/([^=]+)=(.+)/;\n+\n+\tmy ($token, $value) = ($1, $2);\n+\tdie \"Unknown search token $1\" unless exists $q{$token};\n+\t$q{$token} = $value;\n+}\n+\n+# build reverse token map\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+# there are CPAN modules to do this better, but we want to avoid\n+# dependencies and generally, complex netrc-style files are rare\n+\n+if ($debug) {\n+\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)'\n+\t foreach sort keys %q;\n+}\n+\n+LINE: foreach my $line (@data) {\n+\n+\tprint STDERR \"line [$line]\\n\" if $debug;\n+\tmy @tok;\n+\t# gratefully stolen from Net::Netrc\n+\twhile (length $line &&\n+\t       $line =~ s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\tpush(@tok, $tok);\n+\t}\n+\n+\t# skip blank lines, comments, etc.\n+\tnext LINE unless scalar @tok;\n+\n+\tmy %tokens;\n+\twhile (@tok) {\n+\t\tmy ($k, $v) = (shift @tok, shift @tok);\n+\t\tnext unless defined $v;\n+\t\tnext unless exists $options{tmap}->{$k};\n+\t\t$tokens{$options{tmap}->{$k}} = $v;\n+\t}\n+\n+\tforeach my $check (sort keys %q) {\n+\t\tif (exists $tokens{$check} && defined $q{$check}) {\n+\t\t\tprint STDERR \"comparing [$tokens{$check}] to [$q{$check}] in line [$line]\\n\" if $debug;\n+\t\t\tnext LINE unless $tokens{$check} eq $q{$check};\n+\t\t}\n+\t\telse {\n+\t\t\tprint STDERR \"we could not find [$check] but it's OK\\n\" if $debug;\n+\t\t}\n+\t}\n+\n+\tprint STDERR \"line has passed all the search checks\\n\" if $debug;\n+ TOKEN:\n+\tforeach my $token (sort keys %rmap) {\n+\t\tprint STDERR \"looking for useful token $token\\n\" if $debug;\n+\t\tnext unless exists $tokens{$token}; # did we match?\n+\n+\t\tforeach my $rctoken (@{$rmap{$token}}) {\n+\t\t\tnext TOKEN if defined $q{$rctoken};           # don't re-print given tokens\n+\t\t}\n+\n+\t\tprint STDERR \"FOUND: $token=$tokens{$token}\\n\" if $debug;\n+\t\tprintf \"%s=%s\\n\", $token, $tokens{$token};\n+\t}\n+\n+\tlast;\n+}\n+\n+sub load {\n+\t# this supports pipes too\n+\tmy $io = new IO::File(@_) or die \"Could not open [@_]: $!\\n\";\n+\treturn <$io>;                          # whole file\n+}\n-- \n1.7.9.rc2\n"},{"id":"208648","messageId":"20130204211726.GB13186@sigill.intra.peff.net","threadId":"32825","inReplyTo":"87ehgvua6h.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2013-02-04T21:17:26Z","receivedAt":"2013-02-04T21:17:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 04, 2013 at 02:54:30PM -0500, Ted Zlatanov wrote:\n\n> +\tprint <<EOHIPPUS;\n> +\n> +$0 [-f AUTHFILE] [-d] get\n> +\n> +Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n\nThis here-doc is interpolated so you can use $0 and $VERSION, and\ntherefore have to quote the @-sign. But later in the here-doc...\n\n> +Thus, when we get \"protocol=https\\nusername=tzz\", this credential\n> +helper will look for lines in AUTHFILE that match\n\nDo you need to quote \"\\n\" here?\n\n> +die \"Sorry, you need to specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\"\n> + unless defined $file;\n> +\n> +unless (-f $file) {\n> +\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n> +\texit 0;\n> +}\n\nHmm, so it's not an error (just a warning) to say:\n\n  git credential-netrc -f /does/not/exist\n\nbut it is an error to say:\n\n  git credential-netrc\n\nand have it fail to find any netrc files. Shouldn't the latter be a\nlesser error than the former?\n\n> +while (<STDIN>) {\n> +\tnext unless m/([^=]+)=(.+)/;\n> +\n> +\tmy ($token, $value) = ($1, $2);\n> +\tdie \"Unknown search token $1\" unless exists $q{$token};\n> +\t$q{$token} = $value;\n> +}\n\nShould this regex be anchored at the start of the string? I think the\nleft-to-right matching means we will correctly match:\n\n  key=value with=in it\n\nso it may be OK.\n\n> +if ($debug) {\n> +\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)'\n> +\t foreach sort keys %q;\n> +}\n\nLeftover one-char indent.\n\n> [...]\n\nThe rest looks OK to me.\n"},{"id":"208650","messageId":"87vca7sr26.fsf@lifelogs.com","threadId":"32825","inReplyTo":"20130204211726.GB13186@sigill.intra.peff.net","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T21:32:49Z","receivedAt":"2013-02-04T21:32:49Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Mon, 4 Feb 2013 16:17:26 -0500 Jeff King <peff@peff.net> wrote: \n\nJK> Do you need to quote \"\\n\" here?\n\nFixed.\n\nJK> Hmm, so it's not an error (just a warning) to say:\n\nJK>   git credential-netrc -f /does/not/exist\n\nJK> but it is an error to say:\n\nJK>   git credential-netrc\n\nJK> and have it fail to find any netrc files. Shouldn't the latter be a\nJK> lesser error than the former?\n\nFixed, they should both exit(0).\n\n>> +\tnext unless m/([^=]+)=(.+)/;\n\nJK> Should this regex be anchored at the start of the string?\n\nFixed.\n\n>> +\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)'\n>> +\t foreach sort keys %q;\nJK> Leftover one-char indent.\n\nFixed.\n\nTed\n"},{"id":"208652","messageId":"87mwvjsqjc.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"20130204211726.GB13186@sigill.intra.peff.net","subject":"[PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T21:44:07Z","receivedAt":"2013-02-04T21:44:07Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\n contrib/credential/netrc/git-credential-netrc |  236 +++++++++++++++++++++++++\n 1 files changed, 236 insertions(+), 0 deletions(-)\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..d265fde\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,236 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+               help => 0,\n+               debug => 0,\n+\n+               # identical token maps, e.g. host -> host, will be inserted later\n+               tmap => {\n+                        port => 'protocol',\n+                        machine => 'host',\n+                        path => 'path',\n+                        login => 'username',\n+                        user => 'username',\n+                        password => 'password',\n+                       }\n+              );\n+\n+# map each credential protocol token to itself on the netrc side\n+$options{tmap}->{$_} = $_ foreach values %{$options{tmap}};\n+\n+foreach my $suffix ('.gpg', '') {\n+\tforeach my $base (qw/authinfo netrc/) {\n+\t\tmy $file = glob(\"~/.$base$suffix\");\n+\t\tnext unless (defined $file && -f $file);\n+\t\t$options{file} = $file ;\n+\t}\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"file|f=s\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE] [-d] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+  -f AUTHFILE: specify a netrc-style file\n+  -d: turn on debugging\n+\n+To enable (note that Git will prepend \"git-credential-\" to the helper\n+name and look for it in the path):\n+\n+  git config credential.helper '$shortname -f AUTHFILE'\n+\n+And if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens\n+AUTHFILE and looks for entries that match the requested search\n+criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for lines in AUTHFILE that match\n+\n+port https login tzz\n+\n+OR\n+\n+protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the line,\n+including \"password\" tokens, mapping e.g. \"port\" back to \"protocol\".\n+\n+The first matching line is used.  Tokens can be quoted as 'STRING' or\n+\"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# credentials may get 'get', 'store', or 'erase' as parameters but\n+# only acknowledge 'get'\n+die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n+\n+# only support 'get' mode\n+exit unless $mode eq 'get';\n+\n+my $debug = $options{debug};\n+my $file = $options{file};\n+\n+unless (defined $file) {\n+\tprint STDERR \"Please specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\\n\" if $debug;\n+\texit 0;\n+}\n+\n+unless (-f $file) {\n+\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n+\texit 0;\n+}\n+\n+my @data;\n+if ($file =~ m/\\.gpg$/) {\n+\t@data = load('-|', qw(gpg --decrypt), $file)\n+}\n+else {\n+\t@data = load('<', $file);\n+}\n+\n+chomp @data;\n+\n+unless (scalar @data) {\n+\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n+\texit;\n+}\n+\n+# the query: start with every token with no value\n+my %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+while (<STDIN>) {\n+\tnext unless m/^([^=]+)=(.+)/;\n+\n+\tmy ($token, $value) = ($1, $2);\n+\tdie \"Unknown search token $1\" unless exists $q{$token};\n+\t$q{$token} = $value;\n+}\n+\n+# build reverse token map\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+# there are CPAN modules to do this better, but we want to avoid\n+# dependencies and generally, complex netrc-style files are rare\n+\n+if ($debug) {\n+\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)'\n+\t\tforeach sort keys %q;\n+}\n+\n+LINE: foreach my $line (@data) {\n+\n+\tprint STDERR \"line [$line]\\n\" if $debug;\n+\tmy @tok;\n+\t# gratefully stolen from Net::Netrc\n+\twhile (length $line &&\n+\t       $line =~ s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\tpush(@tok, $tok);\n+\t}\n+\n+\t# skip blank lines, comments, etc.\n+\tnext LINE unless scalar @tok;\n+\n+\tmy %tokens;\n+\tmy $num_port;\n+\twhile (@tok) {\n+\t\tmy ($k, $v) = (shift @tok, shift @tok);\n+\t\tnext unless defined $v;\n+\t\tnext unless exists $options{tmap}->{$k};\n+\t\t$tokens{$options{tmap}->{$k}} = $v;\n+\t\t$num_port = $v if $k eq 'port' && $v =~ m/^\\d+$/;\n+\t}\n+\n+\t# for \"host X port Y\" where Y is an integer (captured by\n+\t# $num_port above), set the host to \"X:Y\"\n+\t$tokens{host} = join(':', $tokens{host}, $num_port)\n+\t\tif defined $tokens{host} && defined $num_port;\n+\n+\tforeach my $check (sort keys %q) {\n+\t\tif (exists $tokens{$check} && defined $q{$check}) {\n+\t\t\tprint STDERR \"comparing [$tokens{$check}] to [$q{$check}] in line [$line]\\n\" if $debug;\n+\t\t\tnext LINE unless $tokens{$check} eq $q{$check};\n+\t\t}\n+\t\telse {\n+\t\t\tprint STDERR \"we could not find [$check] but it's OK\\n\" if $debug;\n+\t\t}\n+\t}\n+\n+\tprint STDERR \"line has passed all the search checks\\n\" if $debug;\n+ TOKEN:\n+\tforeach my $token (sort keys %rmap) {\n+\t\tprint STDERR \"looking for useful token $token\\n\" if $debug;\n+\t\tnext unless exists $tokens{$token}; # did we match?\n+\n+\t\tforeach my $rctoken (@{$rmap{$token}}) {\n+\t\t\tnext TOKEN if defined $q{$rctoken};           # don't re-print given tokens\n+\t\t}\n+\n+\t\tprint STDERR \"FOUND: $token=$tokens{$token}\\n\" if $debug;\n+\t\tprintf \"%s=%s\\n\", $token, $tokens{$token};\n+\t}\n+\n+\tlast;\n+}\n+\n+sub load {\n+\t# this supports pipes too\n+\tmy $io = new IO::File(@_) or die \"Could not open [@_]: $!\\n\";\n+\treturn <$io>;                          # whole file\n+}\n-- \n1.7.9.rc2\n"},{"id":"208653","messageId":"7vd2wf1yex.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87mwvjsqjc.fsf_-_@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-04T22:56:06Z","receivedAt":"2013-02-04T22:56:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> Signed-off-by: Ted Zlatanov <tzz@lifelogs.com>\n\nThe space above your S-o-b: could be utilized a bit better ;-)\n\n> @@ -0,0 +1,236 @@\n> +#!/usr/bin/perl\n> +\n> +use strict;\n> +use warnings;\n> +\n> +use Getopt::Long;\n> +use File::Basename;\n> +\n> +my $VERSION = \"0.1\";\n> +\n> +my %options = (\n> +               help => 0,\n> +               debug => 0,\n> +\n> +               # identical token maps, e.g. host -> host, will be inserted later\n> +               tmap => {\n> +                        port => 'protocol',\n> +                        machine => 'host',\n> +                        path => 'path',\n> +                        login => 'username',\n> +                        user => 'username',\n> +                        password => 'password',\n> +                       }\n> +              );\n> +\n> +# map each credential protocol token to itself on the netrc side\n> +$options{tmap}->{$_} = $_ foreach values %{$options{tmap}};\n> +\n> +foreach my $suffix ('.gpg', '') {\n> +\tforeach my $base (qw/authinfo netrc/) {\n> +\t\tmy $file = glob(\"~/.$base$suffix\");\n> +\t\tnext unless (defined $file && -f $file);\n> +\t\t$options{file} = $file ;\n> +\t}\n> +}\n\nThis checks .gpg first and then unencrypted, and checks authinfo\nfirst and netrc second, both of which makes sense.  It is good to\nencourage use of encrypted files, and it is good to use newer\nauthinfo files over netrc files.\n\nHowever, it is strange that you let the ones that are discovered\nlater in the loop to override the ones that are discovered earlier.\nPerhaps you meant\n\n\tnext unless (... exists there ...);\n        $options{\"file\"} = $file;\n        last;\n\ninstead?\n\n> +Getopt::Long::Configure(\"bundling\");\n\nHmm, OK.\n\n> +# TODO: maybe allow the token map $options{tmap} to be configurable.\n> +GetOptions(\\%options,\n> +           \"help|h\",\n> +           \"debug|d\",\n> +           \"file|f=s\",\n> +          );\n> +\n> +if ($options{help}) {\n> +\tmy $shortname = basename($0);\n> +\t$shortname =~ s/git-credential-//;\n> +\n> +\tprint <<EOHIPPUS;\n> +\n> +$0 [-f AUTHFILE] [-d] get\n> +\n> +Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n> +\n> +Options:\n> +  -f AUTHFILE: specify a netrc-style file\n> +  -d: turn on debugging\n> +\n> +To enable (note that Git will prepend \"git-credential-\" to the helper\n> +name and look for it in the path):\n> +\n> +  git config credential.helper '$shortname -f AUTHFILE'\n> +\n> +And if you want lots of debugging info:\n> +\n> +  git config credential.helper '$shortname -f AUTHFILE -d'\n> +\n> +Only \"get\" mode is supported by this credential helper.  It opens\n> +AUTHFILE and looks for entries that match the requested search\n> +criteria:\n> +\n> + 'port|protocol':\n> +   The protocol that will be used (e.g., https). (protocol=X)\n> +\n> + 'machine|host':\n> +   The remote hostname for a network credential. (host=X)\n> +\n> + 'path':\n> +   The path with which the credential will be used. (path=X)\n> +\n> + 'login|user|username':\n> +   The credential’s username, if we already have one. (username=X)\n> +\n> +Thus, when we get this query on STDIN:\n> +\n> +protocol=https\n> +username=tzz\n> +\n> +this credential helper will look for lines in AUTHFILE that match\n> +\n> +port https login tzz\n> +\n> +OR\n> +\n> +protocol https login tzz\n> +\n> +OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n> +simply ignored.\n\nI recall that netrc/authinfo files are _not_ line oriented.  Earlier\nyou said \"looks for entries that match\" which is a lot more correct,\nbut then we see \"look for lines in authfile\".\n\n> +Then, the helper will print out whatever tokens it got from the line,\n> +including \"password\" tokens, mapping e.g. \"port\" back to \"protocol\".\n\nAgain \"line\" is mentioned twice, above and below.\n\n> +The first matching line is used.  Tokens can be quoted as 'STRING' or\n> +\"STRING\".\n> +\n> +No caching is performed by this credential helper.\n> +\n> +EOHIPPUS\n> +\n> +\texit;\n> +}\n\n> +my $mode = shift @ARGV;\n> +\n> +# credentials may get 'get', 'store', or 'erase' as parameters but\n> +# only acknowledge 'get'\n> +die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n> +\n> +# only support 'get' mode\n> +exit unless $mode eq 'get';\n\nThe above looks strange.  Why does the invoker get the error message\nonly when it runs this without arguments?  Did you mean to say more\nlike this?\n\n\tunless (defined $mode && $mode eq 'get') {\n\t\tdie \"...\";\n\t}\n\nBy the way, I think statement modifiers tend to get overused and\nmake the resulting program harder to read.  die \"...\" at the\nbeginning of line makes the reader go \"Whoa, it already is done and\nexisting on error\", and then forces the eyes to scan the error\nmessage to find \"unless\" and the condition.\n\nIt may be a cute syntax and some may find it even cool, but cuteness\nor coolness is less valuable compared with the readability.\n\n> +my $debug = $options{debug};\n> +my $file = $options{file};\n> +\n> +unless (defined $file) {\n> +\tprint STDERR \"Please specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\\n\" if $debug;\n> +\texit 0;\n> +}\n> +\n> +unless (-f $file) {\n> +\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n> +\texit 0;\n> +}\n\nPerhaps \"-r $file\", if you say \"is not accessible\"?\n\nIs it sensible to squelch the error message by default and force\nuser to specify --debug?  You could argue that the option is to\ndebug the user's configuration, but the name of the option sounds\nmore like it is for debugging this script itself.\n\nI saw Peff already pointed out error conditions, but I am not sure\nwhy all of these exit with 0.  If the user has configured\n\n\tgit config credential.helper 'netrc -f $HOME/.netcr'\n\nshouldn't it be diagnosed as an error?  It is understandable to let\nthis go silently\n\n\tgit config credential.helper 'netrc'\n\nand let other credential helpers take over when no $HOME/.{netrc,authinfo}{,.gpg}\nfile exist, but in that case the user may still want to remove the\nconfig item that is not doing anything useful and erroring out with\na message may be a way to help the user know about the situation.\n\n> +my @data;\n> +if ($file =~ m/\\.gpg$/) {\n> +\t@data = load('-|', qw(gpg --decrypt), $file)\n> +}\n> +else {\n> +\t@data = load('<', $file);\n> +}\n> +\n> +chomp @data;\n> +\n> +unless (scalar @data) {\n\nShouldn't this error check come logically before chomping?\n\n> +\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n> +\texit;\n> +}\n\nIs this really an error?  The file perhaps was empty.  Shouldn't\nthat case treated the same way as the case where no entry that\nmatches the criteria invoker gave you was found?\n"},{"id":"208655","messageId":"20130204232317.GA17705@sigill.intra.peff.net","threadId":"32825","inReplyTo":"7vd2wf1yex.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2013-02-04T23:23:17Z","receivedAt":"2013-02-04T23:23:17Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Feb 04, 2013 at 02:56:06PM -0800, Junio C Hamano wrote:\n\n> > +my $mode = shift @ARGV;\n> > +\n> > +# credentials may get 'get', 'store', or 'erase' as parameters but\n> > +# only acknowledge 'get'\n> > +die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n> > +\n> > +# only support 'get' mode\n> > +exit unless $mode eq 'get';\n> \n> The above looks strange.  Why does the invoker get the error message\n> only when it runs this without arguments?  Did you mean to say more\n> like this?\n> \n> \tunless (defined $mode && $mode eq 'get') {\n> \t\tdie \"...\";\n> \t}\n\nNot having a mode is an invocation error; the credential-helper\ndocumentation indicates that the helper will always be invoked with an\naction. The likely culprit for not having one is the user invoking it\nmanually, and showing the usage there is a sensible action.\n\nWhereas invoking it with a mode other than \"get\" is not an error at all.\nGit will run it with the \"store\" and \"erase\" actions, too. Those happen\nto be no-ops for this helper, so it exits silently. The credential docs\nspecify that any other actions should be ignored, too, to allow for\nfuture expansion.\n\n> > +my $debug = $options{debug};\n> > +my $file = $options{file};\n> > +\n> > +unless (defined $file) {\n> > +\tprint STDERR \"Please specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\\n\" if $debug;\n> > +\texit 0;\n> > +}\n> > +\n> > +unless (-f $file) {\n> > +\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n> > +\texit 0;\n> > +}\n> \n> Perhaps \"-r $file\", if you say \"is not accessible\"?\n\nEven better: look at whether opening the file was successful. Though I\nguess that is complicated by the use of gpg, who will probably not\ndistinguish ENOENT from other failures for us.\n\n> Is it sensible to squelch the error message by default and force\n> user to specify --debug?  You could argue that the option is to\n> debug the user's configuration, but the name of the option sounds\n> more like it is for debugging this script itself.\n> \n> I saw Peff already pointed out error conditions, but I am not sure\n> why all of these exit with 0.  If the user has configured\n\nIt was from my suggestion to ignore missing files, which is that the\nuser might have the helper configured (e.g., via /etc/gitconfig, or by a\nshared ~/.gitconfig) but not actually have a netrc.\n\nIt gets confusing because the contents of $file may have been\nauto-detected, or it may have come from the command-line, and we do not\nremember which at this point.\n\nI was trying not to be too nit-picky with my review, but here is how I\nwould have written the outer logic of the script:\n\n  my $tokens = read_credential_data_from_stdin();\n  if ($options{file}) {\n          my @entries = load_netrc($options{file})\n                  or die \"unable to open $options{file}: $!\";\n          check_netrc($tokens, @entries);\n  }\n  else {\n          foreach my $ext ('.gpg', '') {\n                  foreach my $base (qw(authinfo netrc)) {\n                          my @entries = load_netrc(\"$base$ext\")\n                                  or next;\n                          if (check_netrc($tokens, @entries)) {\n                                  last;\n                          }\n                  }\n          }\n  }\n\nI.e., to fail on \"-f\", but otherwise treat unreadable auto-selected\nfiles as a no-op, for whatever reason. I'd also consider checking all\nfiles if they are available, in case the user has multiple (e.g., they\nkeep low-quality junk unencrypted but some high-security passwords in a\n.gpg file). Not that likely, but not any harder to implement.\n\n-Peff\n"},{"id":"208656","messageId":"87fw1bslph.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"7vd2wf1yex.fsf@alter.siamese.dyndns.org","subject":"[PATCHv3] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T23:28:26Z","receivedAt":"2013-02-04T23:28:26Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"Changes since PATCHv2:\n\n- don't keep looking at netrc candidates if one good one is found\n\n- fixed wording of \"line\" to \"entry\" everywhere suitable\n\n- many (but not all) statement modifiers changed to block format\n\n- use -r everywhere instead of -f\n\n- move chomp to when we know @data has contents\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\n contrib/credential/netrc/git-credential-netrc |  243 +++++++++++++++++++++++++\n 1 files changed, 243 insertions(+), 0 deletions(-)\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..99ab204\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,243 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+               help => 0,\n+               debug => 0,\n+\n+               # identical token maps, e.g. host -> host, will be inserted later\n+               tmap => {\n+                        port => 'protocol',\n+                        machine => 'host',\n+                        path => 'path',\n+                        login => 'username',\n+                        user => 'username',\n+                        password => 'password',\n+                       }\n+              );\n+\n+# map each credential protocol token to itself on the netrc side\n+foreach (values %{$options{tmap}}) {\n+\t$options{tmap}->{$_} = $_;\n+}\n+\n+FILE:\n+foreach my $suffix ('.gpg', '') {\n+\tforeach my $base (qw/authinfo netrc/) {\n+\t\tmy $file = glob(\"~/.$base$suffix\");\n+\t\tnext unless (defined $file && -r $file);\n+\t\t$options{file} = $file;\n+\t\tlast FILE;\n+\t}\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"file|f=s\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE] [-d] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+  -f AUTHFILE: specify a netrc-style file\n+  -d: turn on debugging\n+\n+To enable (note that Git will prepend \"git-credential-\" to the helper\n+name and look for it in the path):\n+\n+  git config credential.helper '$shortname -f AUTHFILE'\n+\n+And if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens\n+AUTHFILE and looks for entries that match the requested search\n+criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for entries in AUTHFILE that match\n+\n+port https login tzz\n+\n+OR\n+\n+protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the entry,\n+including \"password\" tokens, mapping e.g. \"port\" back to \"protocol\".\n+\n+The first matching entry is used.  Tokens can be quoted as 'STRING' or\n+\"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# credentials may get 'get', 'store', or 'erase' as parameters but\n+# only acknowledge 'get'\n+die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n+\n+# only support 'get' mode\n+exit unless $mode eq 'get';\n+\n+my $debug = $options{debug};\n+my $file = $options{file};\n+\n+unless (defined $file) {\n+\tprint STDERR \"Please specify an existing netrc file (with or without a .gpg extension) with -f AUTHFILE\\n\" if $debug;\n+\texit 0;\n+}\n+\n+unless (-r $file) {\n+\tprint STDERR \"Sorry, the specified netrc $file is not accessible\\n\" if $debug;\n+\texit 0;\n+}\n+\n+my @data;\n+if ($file =~ m/\\.gpg$/) {\n+\t@data = load('-|', qw(gpg --decrypt), $file)\n+}\n+else {\n+\t@data = load('<', $file);\n+}\n+\n+unless (scalar @data) {\n+\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n+\texit;\n+}\n+\n+chomp @data;\n+\n+# the query: start with every token with no value\n+my %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+while (<STDIN>) {\n+\tnext unless m/^([^=]+)=(.+)/;\n+\n+\tmy ($token, $value) = ($1, $2);\n+\tdie \"Unknown search token $1\" unless exists $q{$token};\n+\t$q{$token} = $value;\n+}\n+\n+# build reverse token map\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+# there are CPAN modules to do this better, but we want to avoid\n+# dependencies and generally, complex netrc-style files are rare\n+\n+if ($debug) {\n+\tforeach (sort keys %q) {\n+\t\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)';\n+\t}\n+}\n+\n+LINE: foreach my $line (@data) {\n+\n+\tprint STDERR \"line [$line]\\n\" if $debug;\n+\tmy @tok;\n+\t# gratefully stolen from Net::Netrc\n+\twhile (length $line &&\n+\t       $line =~ s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\tpush(@tok, $tok);\n+\t}\n+\n+\t# skip blank lines, comments, etc.\n+\tnext LINE unless scalar @tok;\n+\n+\tmy %tokens;\n+\tmy $num_port;\n+\twhile (@tok) {\n+\t\tmy ($k, $v) = (shift @tok, shift @tok);\n+\t\tnext unless defined $v;\n+\t\tnext unless exists $options{tmap}->{$k};\n+\t\t$tokens{$options{tmap}->{$k}} = $v;\n+\t\t$num_port = ($k eq 'port' && $v =~ m/^\\d+$/) ? $v : undef;\n+\t}\n+\n+\t# for \"host X port Y\" where Y is an integer (captured by\n+\t# $num_port above), set the host to \"X:Y\"\n+\tif (defined $tokens{host} && defined $num_port) {\n+\t\t$tokens{host} = join(':', $tokens{host}, $num_port);\n+\t}\n+\n+\tforeach my $check (sort keys %q) {\n+\t\tif (exists $tokens{$check} && defined $q{$check}) {\n+\t\t\tprint STDERR \"comparing [$tokens{$check}] to [$q{$check}] in entry [$line]\\n\" if $debug;\n+\t\t\tnext LINE unless $tokens{$check} eq $q{$check};\n+\t\t}\n+\t\telse {\n+\t\t\tprint STDERR \"we could not find [$check] but it's OK\\n\" if $debug;\n+\t\t}\n+\t}\n+\n+\tprint STDERR \"entry has passed all the search checks\\n\" if $debug;\n+ TOKEN:\n+\tforeach my $token (sort keys %rmap) {\n+\t\tprint STDERR \"looking for useful token $token\\n\" if $debug;\n+\t\tnext unless exists $tokens{$token}; # did we match?\n+\n+\t\tforeach my $rctoken (@{$rmap{$token}}) {\n+\t\t\t# don't re-print given tokens\n+\t\t\tnext TOKEN if defined $q{$rctoken};\n+\t\t}\n+\n+\t\tprint STDERR \"FOUND: $token=$tokens{$token}\\n\" if $debug;\n+\t\tprintf \"%s=%s\\n\", $token, $tokens{$token};\n+\t}\n+\n+\tlast;\n+}\n+\n+sub load {\n+\t# this supports pipes too\n+\tmy $io = new IO::File(@_) or die \"Could not open [@_]: $!\\n\";\n+\treturn <$io>;                          # whole file\n+}\n-- \n1.7.9.rc2\n"},{"id":"208657","messageId":"87bobzslke.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vd2wf1yex.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T23:31:29Z","receivedAt":"2013-02-04T23:31:29Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Mon, 04 Feb 2013 14:56:06 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> I recall that netrc/authinfo files are _not_ line oriented.  Earlier\nJCH> you said \"looks for entries that match\" which is a lot more correct,\nJCH> but then we see \"look for lines in authfile\".\n\nHmm, do you mean backslashed newlines?  I think the Net::Netrc parser\ndoesn't support them, and I haven't seen them in the wild, but I could\nsupport them if you think that's useful.\n\n>> +my $mode = shift @ARGV;\n>> +\n>> +# credentials may get 'get', 'store', or 'erase' as parameters but\n>> +# only acknowledge 'get'\n>> +die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n>> +\n>> +# only support 'get' mode\n>> +exit unless $mode eq 'get';\n\nJCH> The above looks strange.  Why does the invoker get the error message\nJCH> only when it runs this without arguments?  Did you mean to say more\nJCH> like this?\n\nJCH> \tunless (defined $mode && $mode eq 'get') {\nJCH> \t\tdie \"...\";\nJCH> \t}\n\nI mean:\n\n- if the mode is not given, exit badly (since it's required)\n\n- if the mode is given but we don't support it, exit pleasantly\n\nI thought that was the right thing, according to my reading of the\ncredentials API.  If not, I'll be glad to change it.\n\nJCH> By the way, I think statement modifiers tend to get overused and\nJCH> make the resulting program harder to read.  die \"...\" at the\nJCH> beginning of line makes the reader go \"Whoa, it already is done and\nJCH> existing on error\", and then forces the eyes to scan the error\nJCH> message to find \"unless\" and the condition.\n\nJCH> It may be a cute syntax and some may find it even cool, but cuteness\nJCH> or coolness is less valuable compared with the readability.\n\nYour coding guidelines said you prefer one-line if statements, and I\nthought it would be OK to lean on modifiers.  I changed many of the\nmodifiers but not all; please let me know if you'd like me to change\nthem all.  It's no problem.\n\nJCH> Is it sensible to squelch the error message by default and force\nJCH> user to specify --debug?  You could argue that the option is to\nJCH> debug the user's configuration, but the name of the option sounds\nJCH> more like it is for debugging this script itself.\n\nIt's both... without a clear separation because it's such a small\nscript.  Let me know how you'd like to change it, if at all.\n\nJCH> I saw Peff already pointed out error conditions, but I am not sure\nJCH> why all of these exit with 0.  If the user has configured\n\nJCH> \tgit config credential.helper 'netrc -f $HOME/.netcr'\n\nJCH> shouldn't it be diagnosed as an error?  It is understandable to let\nJCH> this go silently\n\nJCH> \tgit config credential.helper 'netrc'\n\nJCH> and let other credential helpers take over when no $HOME/.{netrc,authinfo}{,.gpg}\nJCH> file exist, but in that case the user may still want to remove the\nJCH> config item that is not doing anything useful and erroring out with\nJCH> a message may be a way to help the user know about the situation.\n\nYou and Peff should tell me how it should behave, or perhaps make the\nchanges after it's in.  I'm happy to change it any way you like, but at\nthis point I'm just following instructions, not really contributing,\nabout the exit statuses.  I thought I knew what you wanted 2 iterations\nago :)\n\n>> +\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n>> +\texit;\n\nJCH> Is this really an error?  The file perhaps was empty.  Shouldn't\nJCH> that case treated the same way as the case where no entry that\nJCH> matches the criteria invoker gave you was found?\n\nexit(0) is not an error, so the behavior is exactly the same, we just\ndon't print anything to STDOUT because there was no data, with a nicer\nerror message.  I think that's what we want?\n\nPATCHv3 is out with the rest of your suggestions.  Thank you for the\nthorough review.  I am happy to improve the script to meet your standards.\n\nThanks\nTed\n"},{"id":"208658","messageId":"7v38xb1wk6.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"20130204232317.GA17705@sigill.intra.peff.net","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-04T23:36:09Z","receivedAt":"2013-02-04T23:36:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Mon, Feb 04, 2013 at 02:56:06PM -0800, Junio C Hamano wrote:\n>\n>> > +my $mode = shift @ARGV;\n>> > +\n>> > +# credentials may get 'get', 'store', or 'erase' as parameters but\n>> > +# only acknowledge 'get'\n>> > +die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n>> > +\n>> > +# only support 'get' mode\n>> > +exit unless $mode eq 'get';\n>> \n>> The above looks strange.  Why does the invoker get the error message\n>> only when it runs this without arguments?  Did you mean to say more\n>> like this?\n>> \n>> \tunless (defined $mode && $mode eq 'get') {\n>> \t\tdie \"...\";\n>> \t}\n>\n> Not having a mode is an invocation error; the credential-helper\n> documentation indicates that the helper will always be invoked with an\n> action. The likely culprit for not having one is the user invoking it\n> manually, and showing the usage there is a sensible action.\n>\n> Whereas invoking it with a mode other than \"get\" is not an error at all.\n> Git will run it with the \"store\" and \"erase\" actions, too. Those happen\n> to be no-ops for this helper, so it exits silently. The credential docs\n> specify that any other actions should be ignored, too, to allow for\n> future expansion.\n\nOK.  The code didn't express the above reasoning clearly enough.\n\n> I was trying not to be too nit-picky with my review,...\n\nI wasn't either.  Mine was still at design level review to get the\nsemantics right (e.g. what to consider as errors, the input is _not_\none entry per line, etc.), before reviewing the details of the\nimplementation.\n\n> but here is how I\n> would have written the outer logic of the script:\n>\n>   my $tokens = read_credential_data_from_stdin();\n>   if ($options{file}) {\n>           my @entries = load_netrc($options{file})\n>                   or die \"unable to open $options{file}: $!\";\n>           check_netrc($tokens, @entries);\n>   }\n>   else {\n>           foreach my $ext ('.gpg', '') {\n>                   foreach my $base (qw(authinfo netrc)) {\n>                           my @entries = load_netrc(\"$base$ext\")\n>                                   or next;\n>                           if (check_netrc($tokens, @entries)) {\n>                                   last;\n>                           }\n>                   }\n>           }\n>   }\n>\n> I.e., to fail on \"-f\", but otherwise treat unreadable auto-selected\n> files as a no-op, for whatever reason. I'd also consider checking all\n> files if they are available, in case the user has multiple (e.g., they\n> keep low-quality junk unencrypted but some high-security passwords in a\n> .gpg file). Not that likely, but not any harder to implement.\n\nYeah, I think that looks like the right top-level codeflow.\n"},{"id":"208659","messageId":"7vy5f3zlzj.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87bobzslke.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-04T23:40:32Z","receivedAt":"2013-02-04T23:40:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n>>> +my $mode = shift @ARGV;\n>>> +\n>>> +# credentials may get 'get', 'store', or 'erase' as parameters but\n>>> +# only acknowledge 'get'\n>>> +die \"Syntax: $0 [-f AUTHFILE] [-d] get\" unless defined $mode;\n>>> +\n>>> +# only support 'get' mode\n>>> +exit unless $mode eq 'get';\n>\n> JCH> The above looks strange.  Why does the invoker get the error message\n> JCH> only when it runs this without arguments?  Did you mean to say more\n> JCH> like this?\n>\n> JCH> \tunless (defined $mode && $mode eq 'get') {\n> JCH> \t\tdie \"...\";\n> JCH> \t}\n>\n> I mean:\n>\n> - if the mode is not given, exit badly (since it's required)\n>\n> - if the mode is given but we don't support it, exit pleasantly\n>\n> I thought that was the right thing, according to my reading of the\n> credentials API.  If not, I'll be glad to change it.\n\nAs Peff noted, I mistead what the code was doing, especially with\nsomewhat cryptic \"only support x mode\" comment, as if it is\nrejecting other modes.\n\n>>> +\tprint STDERR \"Sorry, we could not load data from [$file]\\n\" if $debug;\n>>> +\texit;\n>\n> JCH> Is this really an error?  The file perhaps was empty.  Shouldn't\n> JCH> that case treated the same way as the case where no entry that\n> JCH> matches the criteria invoker gave you was found?\n>\n> exit(0) is not an error, so the behavior is exactly the same, we just\n> don't print anything to STDOUT because there was no data, with a nicer\n> error message.  I think that's what we want?\n\n\"Sorry we couldn't\" sounded like an error messag to me.  If this is\na normal exit, then please make sure it is a normal exit.\n\nThe review cycle is not like reviewers give you instructions and\ndesigns and you blindly implement them.  It is a creative process\nwhere you show the design and a clear implementation of that design.\n\nThanks.\n"},{"id":"208660","messageId":"877gmnsl2q.fsf@lifelogs.com","threadId":"32825","inReplyTo":"20130204232317.GA17705@sigill.intra.peff.net","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T23:42:05Z","receivedAt":"2013-02-04T23:42:05Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Mon, 4 Feb 2013 18:23:17 -0500 Jeff King <peff@peff.net> wrote: \n\n>> Perhaps \"-r $file\", if you say \"is not accessible\"?\n\nJK> Even better: look at whether opening the file was successful. Though I\nJK> guess that is complicated by the use of gpg, who will probably not\nJK> distinguish ENOENT from other failures for us.\n\nYup.  I think the outcome for the user will be the same, so this is\nmostly for debugging, right?  And we do look at the outcome of opening\nthe file, and die if that failed (which would change if your suggestion\nbelow is implemented).\n\nJK> I was trying not to be too nit-picky with my review, but here is how I\nJK> would have written the outer logic of the script:\n\nJK>   my $tokens = read_credential_data_from_stdin();\nJK>   if ($options{file}) {\nJK>           my @entries = load_netrc($options{file})\nJK>                   or die \"unable to open $options{file}: $!\";\nJK>           check_netrc($tokens, @entries);\nJK>   }\nJK>   else {\nJK>           foreach my $ext ('.gpg', '') {\nJK>                   foreach my $base (qw(authinfo netrc)) {\nJK>                           my @entries = load_netrc(\"$base$ext\")\nJK>                                   or next;\nJK>                           if (check_netrc($tokens, @entries)) {\nJK>                                   last;\nJK>                           }\nJK>                   }\nJK>           }\nJK>   }\n\nJK> I.e., to fail on \"-f\", but otherwise treat unreadable auto-selected\nJK> files as a no-op, for whatever reason.\n\nJK> I'd also consider checking all files if they are available, in case\nJK> the user has multiple (e.g., they keep low-quality junk unencrypted\nJK> but some high-security passwords in a .gpg file). Not that likely,\nJK> but not any harder to implement.\n\nI think that makes everything more complicated, and the user can name a\nspecific netrc file in the helper spec if he wants it.  It's too\nautomagic for me.  But if you and Junio feel this is the right approach,\nI'll rewrite to basically allow --file to take a list of filenames and\ndefault that list to the base list of ~/.{authinfo,netrc}{,.gpg}\n\nTed\n"},{"id":"208662","messageId":"87zjzjr5y4.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vy5f3zlzj.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-04T23:54:11Z","receivedAt":"2013-02-04T23:54:11Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Mon, 04 Feb 2013 15:40:32 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> \"Sorry we couldn't\" sounded like an error messag to me.  If this is\nJCH> a normal exit, then please make sure it is a normal exit.\n\nOK; done in PATCHv4: removed all \"Sorry\" because they are not abnormal\nexits.  I'll hold PATCHv4 until the below are known.\n\nJCH> The review cycle is not like reviewers give you instructions and\nJCH> designs and you blindly implement them.  It is a creative process\nJCH> where you show the design and a clear implementation of that design.\n\nOK.  I would like you to make the decisions I asked for, though:\n\n- do you want to support backslashed newlines?\n- do you want me to remove the statement modifiers?\n- should all die() calls just print to STDERR and exit(0)?\n- do you want to support multiple netrc files, as you and Peff suggested?\n\nOn all of those, I can go either way, it's just a little more work for me.\n\nTed\n"},{"id":"208664","messageId":"7vmwvjzkcs.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87zjzjr5y4.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T00:15:47Z","receivedAt":"2013-02-05T00:15:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> On Mon, 04 Feb 2013 15:40:32 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n>\n> JCH> \"Sorry we couldn't\" sounded like an error messag to me.  If this is\n> JCH> a normal exit, then please make sure it is a normal exit.\n>\n> OK; done in PATCHv4: removed all \"Sorry\" because they are not abnormal\n> exits.  I'll hold PATCHv4 until the below are known.\n>\n> JCH> The review cycle is not like reviewers give you instructions and\n> JCH> designs and you blindly implement them.  It is a creative process\n> JCH> where you show the design and a clear implementation of that design.\n>\n> OK.  I would like you to make the decisions I asked for, though:\n>\n> - do you want to support backslashed newlines?\n\nWhat for?  netrc/authinfo is not a line oriented file format at all,\nand\n\n\tmachine k.org\n\t        \tlogin me\n                        password mysecret\n\nis a single entry; you do not need backslash at the end of any line.\n\nPerhaps you are asking something different?\n\n> - do you want me to remove the statement modifiers?\n\nI do not think we are at that \"implementation nitpick\" level yet.\n\n> - should all die() calls just print to STDERR and exit(0)?\n\nWhere \"when unhandled, the helper should silently exit with 0\" is\nexpected by the invoker, we shouldn't say anything to error stream,\nand exit with zero.  Please leave a comment to make it easy to\nunderstand to the readers that is what is going on there.\n\nIf on the other hand it diagnosed an error (not a bug in the\nimplementation but a misconfiguration on the user's side), I _think_\nit should loudly die() so that the user can notice and take\ncorrective action.\n\n> - do you want to support multiple netrc files, as you and Peff suggested?\n\nI didn't even suggest such thing IIRC---I expected it to iterate\nfrom the most desirable (.authinfo.gpg) to the least (.netrc) and\nstop at the first found one.  There may be use cases people use more\nthan one and expect an entry to be found in any file, but I suspect\nthat might be more confusing than it is worth.  But I do not care\nvery deeply myself either way.\n"},{"id":"208726","messageId":"87sj5ariar.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vmwvjzkcs.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T13:39:40Z","receivedAt":"2013-02-05T13:39:40Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Mon, 04 Feb 2013 16:15:47 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\n\n>> - do you want to support backslashed newlines?\n\nJCH> What for?  netrc/authinfo is not a line oriented file format at all,\nJCH> and\n\nJCH> \tmachine k.org\nJCH> \t        \tlogin me\nJCH>                         password mysecret\n\nJCH> is a single entry; you do not need backslash at the end of any line.\n\nHmm. The parser I implemented only does single-line parsing, and I\nmisunderstood the format to be single-line (partly because I have never\nseen anyone using the multi-line format you show).  Looking at\nNet::Netrc more carefully, it seems that the \"machine\" token is what\ndefines an entry, so a new entry starts with a new line that contains a\n\"machine\" token.  Is that acceptable and does it match your\nunderstanding of the format?  It matches Net::Netrc, at least.\n\nI'll add this change to PATCHv4 with the assumption you agree.\n\n>> - should all die() calls just print to STDERR and exit(0)?\n\nJCH> Where \"when unhandled, the helper should silently exit with 0\" is\nJCH> expected by the invoker, we shouldn't say anything to error stream,\nJCH> and exit with zero.  Please leave a comment to make it easy to\nJCH> understand to the readers that is what is going on there.\n\nJCH> If on the other hand it diagnosed an error (not a bug in the\nJCH> implementation but a misconfiguration on the user's side), I _think_\nJCH> it should loudly die() so that the user can notice and take\nJCH> corrective action.\n\nOK, I'll review these for PATCHv4 (also see below).  Thanks.\n\n>> - do you want to support multiple netrc files, as you and Peff suggested?\n\nJCH> I didn't even suggest such thing IIRC---I expected it to iterate\nJCH> from the most desirable (.authinfo.gpg) to the least (.netrc) and\nJCH> stop at the first found one.  There may be use cases people use more\nJCH> than one and expect an entry to be found in any file, but I suspect\nJCH> that might be more confusing than it is worth.  But I do not care\nJCH> very deeply myself either way.\n\nAfter thinking about it, I agree with Peff multiple sources make sense\nand will simplify the code flow (especially the default case, which\nwon't need to be handled separately).  And the functionality doesn't\nhave to be confusing with the right debugging messages.  So I'll add\nthem in PATCHv4.\n\nThe debugging messages will be fewer and simpler with this approach,\nwhich makes it feel like the right track :)\n\nThanks\nTed\n"},{"id":"208735","messageId":"7v7gmmvj5p.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87sj5ariar.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T16:07:30Z","receivedAt":"2013-02-05T16:07:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> On Mon, 04 Feb 2013 16:15:47 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n>\n> JCH> Ted Zlatanov <tzz@lifelogs.com> writes:\n>\n>>> - do you want to support backslashed newlines?\n>\n> JCH> What for?  netrc/authinfo is not a line oriented file format at all,\n> JCH> and\n>\n> JCH> \tmachine k.org\n> JCH> \t        \tlogin me\n> JCH>                         password mysecret\n>\n> JCH> is a single entry; you do not need backslash at the end of any line.\n>\n> Hmm. The parser I implemented only does single-line parsing, and I\n> misunderstood the format to be single-line (partly because I have never\n> seen anyone using the multi-line format you show).  Looking at\n> Net::Netrc more carefully, it seems that the \"machine\" token is what\n> defines an entry, so a new entry starts with a new line that contains a\n> \"machine\" token.  Is that acceptable and does it match your\n> understanding of the format?  It matches Net::Netrc, at least.\n\nI thought I've given a more concrete outline than \"I'll read\nNet::Netrc and do whatever I think it does\" in a separate message.\n\nIt would be better to read \"man netrc\" carefully at least once ;-)\n"},{"id":"208738","messageId":"7vvca6u47f.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87mwvjsqjc.fsf_-_@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T16:15:48Z","receivedAt":"2013-02-05T16:15:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> +# build reverse token map\n> +my %rmap;\n> +foreach my $k (keys %{$options{tmap}}) {\n> +\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n> +}\n\nMental note: \"$rmap{foo} -eq 'bar'\" means that what Git calls 'bar'\nis found as 'foo' in the netrc/authinfo file.  Keys in %rmap are\nwhat we expect to read from the netrc/authinfo file.\n\n> +# there are CPAN modules to do this better, but we want to avoid\n> +# dependencies and generally, complex netrc-style files are rare\n> +\n> +if ($debug) {\n> +\tprintf STDERR \"searching for %s = %s\\n\", $_, $q{$_} || '(any value)'\n> +\t\tforeach sort keys %q;\n> +}\n> +\n> +LINE: foreach my $line (@data) {\n> +\n> +\tprint STDERR \"line [$line]\\n\" if $debug;\n> +\tmy @tok;\n> +\t# gratefully stolen from Net::Netrc\n> +\twhile (length $line &&\n> +\t       $line =~ s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n> +\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n> +\t\tpush(@tok, $tok);\n> +\t}\n> +\n> +\t# skip blank lines, comments, etc.\n> +\tnext LINE unless scalar @tok;\n> +\n> +\tmy %tokens;\n> +\tmy $num_port;\n> +\twhile (@tok) {\n> +\t\tmy ($k, $v) = (shift @tok, shift @tok);\n> +\t\tnext unless defined $v;\n> +\t\tnext unless exists $options{tmap}->{$k};\n> +\t\t$tokens{$options{tmap}->{$k}} = $v;\n> +\t\t$num_port = $v if $k eq 'port' && $v =~ m/^\\d+$/;\n> +\t}\n\nSo you grabbed one line of input, split them into token pairs, and\nbuilt %tokens = ('key Git may want to see' => 'value read from file')\nmapping.\n\n> +\t# for \"host X port Y\" where Y is an integer (captured by\n> +\t# $num_port above), set the host to \"X:Y\"\n> +\t$tokens{host} = join(':', $tokens{host}, $num_port)\n> +\t\tif defined $tokens{host} && defined $num_port;\n\nWhat happens when 'host' does not exist?  netrc/authinfo should be a\nstream of SP/HT/LF delimited tokens and 'machine' token (or\n'default') begins a new entry, so it would mean the input file is\ncorrupt if we do not have $tokens{host} when we get here, I think.\n\nOh, another thing. 'default' is like 'machine' followed by any\nmachine name, so the above while loop that reads two tokens\npair-wise needs to be aware that 'default' is not followed by a\nvalue.  I think the loop will fail to parse this:\n\n        default       login anonymous    password me@home\n        machine k.org login me           password mysecret\n\n> +\tforeach my $check (sort keys %q) {\n\nHmph, aren't you checking what you read a bit too early?  This is a\nvalid input:\n\n        default       \n                login anonymous\n                password me@home\n        machine k.org\n                login me\n                password mysecret\n\nbut does this loop gives mysecret back to me when asked for\nhost=k.org and user=me? \n\n> +\t\tif (exists $tokens{$check} && defined $q{$check}) {\n> +\t\t\tprint STDERR \"comparing [$tokens{$check}] to [$q{$check}] in line [$line]\\n\" if $debug;\n> +\t\t\tnext LINE unless $tokens{$check} eq $q{$check};\n> +\t\t}\n> +\t\telse {\n> +\t\t\tprint STDERR \"we could not find [$check] but it's OK\\n\" if $debug;\n> +\t\t}\n> +\t}\n\nI would probably structure this part like this:\n\n\t%pending = ();\n        split the whole input into tokens, regardless of lines;\n        iterate over the tokens {\n\t\tpeek the token\n\t\tif (it is not \"default\") {\n\t\t\ttake (token, value) pair;\n\t\t} else {\n\t\t\ttake \"default\" as token; value does not matter.\n\t\t}\n                if (token is \"default\" or \"machine\") {\n\t\t\t# finished reading one entry and we are\n                        # at the beginning of the next entry.\n                        # see if this entry matches\n\t\t\tif (%pending is not empty &&\n                            %pending matches %q) {\n\t\t\t\tfound a match; use %pending;\n\t\t\t}\n                        # done with that entry. now start a new one.\n                        %pending = ();\n\t\t}\n\t\t$pending{token} = value;\n\t}\n"},{"id":"208739","messageId":"7vr4kuu43h.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"7v7gmmvj5p.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T16:18:10Z","receivedAt":"2013-02-05T16:18:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I thought I've given a more concrete outline than \"I'll read\n> Net::Netrc and do whatever I think it does\" in a separate message.\n\nAnd it turns out that the message was sitting in my outbox.  Sorry.\n\nI just told the outbox to send it out.\n"},{"id":"208744","messageId":"87k3qmr8yc.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vvca6u47f.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T17:01:31Z","receivedAt":"2013-02-05T17:01:31Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 08:15:48 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\n>> +# build reverse token map\n>> +my %rmap;\n>> +foreach my $k (keys %{$options{tmap}}) {\n>> +\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n>> +}\n\nJCH> Mental note: \"$rmap{foo} -eq 'bar'\" means that what Git calls 'bar'\nJCH> is found as 'foo' in the netrc/authinfo file.  Keys in %rmap are\nJCH> what we expect to read from the netrc/authinfo file.\n\nI'll document that better in PATCHv4.\n\nJCH> So you grabbed one line of input, split them into token pairs, and\nJCH> built %tokens = ('key Git may want to see' => 'value read from file')\nJCH> mapping.\n\nThis will be fixed with PATCHv4 to do multiple lines (as defined by the\nnetrc manpage, etc.).\n\n>> +\t# for \"host X port Y\" where Y is an integer (captured by\n>> +\t# $num_port above), set the host to \"X:Y\"\n>> +\t$tokens{host} = join(':', $tokens{host}, $num_port)\n>> +\t\tif defined $tokens{host} && defined $num_port;\n\nJCH> What happens when 'host' does not exist?  netrc/authinfo should be a\nJCH> stream of SP/HT/LF delimited tokens and 'machine' token (or\nJCH> 'default') begins a new entry, so it would mean the input file is\nJCH> corrupt if we do not have $tokens{host} when we get here, I think.\n\nYes.  I'll make the host/machine token required, which will avoid this\nissue.\n\nJCH> Oh, another thing. 'default' is like 'machine' followed by any\nJCH> machine name, so the above while loop that reads two tokens\nJCH> pair-wise needs to be aware that 'default' is not followed by a\nJCH> value.  I think the loop will fail to parse this:\n\nJCH>         default       login anonymous    password me@home\nJCH>         machine k.org login me           password mysecret\n\nI'd prefer to ignore \"default\" because it should not be used for the Git\ncredential helpers (its only use case is for anonymous services AFAIK).\nSo I'll add a case to ignore it in PATCHv4, if that's OK.\n\nJCH> Hmph, aren't you checking what you read a bit too early?  This is a\nJCH> valid input:\n\nJCH>         default       \nJCH>                 login anonymous\nJCH>                 password me@home\nJCH>         machine k.org\nJCH>                 login me\nJCH>                 password mysecret\n\nJCH> but does this loop gives mysecret back to me when asked for\nJCH> host=k.org and user=me? \n\nTo be fixed in PATCHv4, which will require the host/machine, use it as\nthe primary key, and only examine entries with it.\n\nJCH> I would probably structure this part like this: [...]\n\nI will do it like that, thank you for the suggestion.\n\nI'll also add a simple testing Makefile for my own use, and you can\nconsider adding tests to the general framework later.\n\nTed\n"},{"id":"208751","messageId":"87fw1ar3og.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"87k3qmr8yc.fsf@lifelogs.com","subject":"[PATCHv4] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T18:55:27Z","receivedAt":"2013-02-05T18:55:27Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"Changes since PATCHv3:\n\n- simple tests in Makefile\n- support multiple files, code refactored\n- documentation and comments updated\n- fix IO::File for GPG pipe\n- exit peacefully in almost every situation, die on bad invocation or query\n- use log_verbose() and -v for logging for the user\n- use log_debug() and -d for logging for the developer\n- use Net::Netrc parser and `man netrc' to improve parsing\n- ignore 'default' and 'macdef' netrc entries\n- require 'machine' token in netrc lines\n- ignore netrc files with bad permissions or owner (from Net::Netrc)\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\n contrib/credential/netrc/Makefile             |   10 +\n contrib/credential/netrc/git-credential-netrc |  423 +++++++++++++++++++++++++\n 2 files changed, 433 insertions(+), 0 deletions(-)\n create mode 100644 contrib/credential/netrc/Makefile\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\nnew file mode 100644\nindex 0000000..ee8c5f0\n--- /dev/null\n+++ b/contrib/credential/netrc/Makefile\n@@ -0,0 +1,10 @@\n+test_netrc:\n+\t@(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring failure\"\n+\t@echo \"-> Silent invocation... nothing should show up here with a missing file\"\n+\t@echo \"bad data\" | ./git-credential-netrc -f A get\n+\t@echo \"-> Back to noisy: -v and -d used below, missing file\"\n+\techo \"bad data\" | ./git-credential-netrc -f A -d -v get\n+\t@echo \"-> Look for any entry in the default file set\"\n+\techo \"\" | ./git-credential-netrc -d -v get\n+\t@echo \"-> Look for github.com in the default file set\"\n+\techo \"host=google.com\" | ./git-credential-netrc -d -v get\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..6946217\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,423 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+               help => 0,\n+               debug => 0,\n+               verbose => 0,\n+\t       file => [],\n+\n+               # identical token maps, e.g. host -> host, will be inserted later\n+               tmap => {\n+                        port => 'protocol',\n+                        machine => 'host',\n+                        path => 'path',\n+                        login => 'username',\n+                        user => 'username',\n+                        password => 'password',\n+                       }\n+              );\n+\n+# Map each credential protocol token to itself on the netrc side.\n+foreach (values %{$options{tmap}}) {\n+\t$options{tmap}->{$_} = $_;\n+}\n+\n+# Now, $options{tmap} has a mapping from the netrc format to the Git credential\n+# helper protocol.\n+\n+# Next, we build the reverse token map.\n+\n+# When $rmap{foo} contains 'bar', that means that what the Git credential helper\n+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in\n+# %rmap are what we expect to read from the netrc/authinfo file.\n+\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"verbose|v\",\n+           \"file|f=s@\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+\n+  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n+                       will be decrypted by GPG before parsing.  Multiple -f\n+                       arguments are OK, and the order is respected.\n+\n+  -d|--debug         : turn on debugging (developer info)\n+\n+  -v|--verbose       : be more verbose (show files and information found)\n+\n+To enable this credential helper:\n+\n+  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'\n+\n+(Note that Git will prepend \"git-credential-\" to the helper name and look for it\n+in the path.)\n+\n+...and if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+...or to see the files opened and data found:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -v'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens every AUTHFILE\n+and looks for the first entry that matches the requested search criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for the first entry in every AUTHFILE that\n+matches\n+\n+port https login tzz\n+\n+OR\n+\n+protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the entry, including\n+\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n+back to \"protocol\".\n+\n+Again, note that the first matching entry from all the AUTHFILEs is used.\n+\n+Tokens can be quoted as 'STRING' or \"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit 0;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# Credentials must get a parameter, so die if it's missing.\n+die \"Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get\" unless defined $mode;\n+\n+# Only support 'get' mode; with any other unsupported ones we just exit.\n+exit 0 unless $mode eq 'get';\n+\n+my $files = $options{file};\n+\n+# if no files were given, use a predefined list.\n+# note that .gpg files come first\n+unless (scalar @$files)\n+{\n+\tmy @candidates = qw[\n+\t\t\t\t   ~/.authinfo.gpg\n+\t\t\t\t   ~/.netrc.gpg\n+\t\t\t\t   ~/.authinfo\n+\t\t\t\t   ~/.netrc\n+\t\t\t  ];\n+\n+\t$files = $options{file} = [ map { glob $_ } @candidates ];\n+}\n+\n+my $query = read_credential_data_from_stdin();\n+\n+FILE:\n+foreach my $file (@$files)\n+{\n+\tunless (-r $file)\n+\t{\n+\t\tlog_verbose(\"Unable to read $file; skipping it\");\n+\t\tnext FILE;\n+\t}\n+\n+\t# the following check is copied from Net::Netrc\n+\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n+\tunless ($^O eq 'os2'\n+\t\t|| $^O eq 'MSWin32'\n+\t\t|| $^O eq 'MacOS'\n+\t\t|| $^O =~ /^cygwin/)\n+\t{\n+\t\tmy @stat = stat($file);\n+\n+\t\tif (@stat) {\n+\t\t\tif ($stat[2] & 077) {\n+\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n+\t\t\t\t\t    $stat[2] & 07777);\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\t\t\tif ($stat[4] != $<) {\n+\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tmy $mode = (stat($file))[2];\n+\tif ($mode & 077)\n+\t{\n+\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n+\t\t\t    $mode & 07777);\n+\t\tnext FILE;\n+\t}\n+\n+\tmy @entries = load_netrc($file);\n+\n+\tunless (scalar @entries)\n+\t{\n+\t\tif ($!)\n+\t\t{\n+\t\t\tlog_verbose(\"Unable to open $file: $!\");\n+\t\t}\n+\t\telse\n+\t\t{\n+\t\t\tlog_verbose(\"No netrc entries found in $file\");\n+\t\t}\n+\n+\t\tnext FILE;\n+\t}\n+\n+\tmy $entry = find_netrc_entry($query, @entries);\n+\tif ($entry)\n+\t{\n+\t\tprint_credential_data($entry, $query);\n+\t\t# we're done!\n+\t\tlast FILE;\n+\t}\n+}\n+\n+exit 0;\n+\n+sub load_netrc\n+{\n+\tmy $file = shift @_;\n+\n+\tmy $io;\n+\tif ($file =~ m/\\.gpg$/) {\n+\t\tlog_verbose(\"Using GPG to open $file\");\n+\t\t# GPG doesn't work well with 2- or 3-argument open\n+\t\t$io = new IO::File(\"gpg --decrypt $file|\");\n+\t}\n+\telse {\n+\t\tlog_verbose(\"Opening $file...\");\n+\t\t$io = new IO::File($file, '<');\n+\t}\n+\n+\t# nothing to do if the open failed (we log the error later)\n+\treturn unless $io;\n+\n+\t# Net::Netrc does this, but the functionality is merged with the file\n+\t# detection logic, so we have to extract just the part we need\n+\tmy @netrc_entries = net_netrc_loader($io);\n+\n+\t# these entries will use the credential helper protocol token names\n+\tmy @entries;\n+\n+\tforeach my $nentry (@netrc_entries) {\n+\t\tmy %entry;\n+\t\tmy $num_port;\n+\n+\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n+\t\t\t$num_port = $nentry->{port};\n+\t\t\tdelete $nentry->{port};\n+\t\t}\n+\n+\t\t# create the new entry for the credential helper protocol\n+\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n+\n+\t\t# for \"host X port Y\" where Y is an integer (captured by\n+\t\t# $num_port above), set the host to \"X:Y\"\n+\t\tif (defined $entry{host} && defined $num_port) {\n+\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n+\t\t}\n+\n+\t\tpush @entries, \\%entry;\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub net_netrc_loader\n+{\n+\tmy $fh = shift @_;\n+\tmy @entries;\n+\tmy ($mach, $macdef, $tok, @tok) = (0, 0);\n+\n+    LINE:\n+\twhile (<$fh>) {\n+\t\tundef $macdef if /\\A\\n\\Z/;\n+\n+\t\tif ($macdef) {\n+\t\t\tpush(@$macdef, $_);\n+\t\t\tnext LINE;\n+\t\t}\n+\n+\t\ts/^\\s*//;\n+\t\tchomp;\n+\n+\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\t\tpush(@tok, $tok);\n+\t\t}\n+\n+\t    TOKEN:\n+\t\twhile (@tok) {\n+\t\t\t$tok = shift(@tok);\n+\n+\t\t\tif ($tok eq \"machine\") {\n+\t\t\t\tmy $host = shift @tok;\n+\t\t\t\t$mach = { machine => $host };\n+\t\t\t\tpush @entries, $mach;\n+\t\t\t}\n+\t\t\telsif (exists $options{tmap}->{$tok}) {\n+\t\t\t\tunless ($mach) {\n+\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\tunless (defined $value) {\n+\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n+\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n+\t\t\t\t$mach->{$tok} = $value;\n+\t\t\t}\n+\t\t\telsif ($tok eq \"macdef\") { # we ignore macros\n+\t\t\t\tnext TOKEN unless $mach;\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\t$mach->{macdef} = {} unless exists $mach->{macdef};\n+\t\t\t\t$macdef = $mach->{machdef}{$value} = [];\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub read_credential_data_from_stdin\n+{\n+\t# the query: start with every token with no value\n+\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+\twhile (<STDIN>) {\n+\t\tnext unless m/^([^=]+)=(.+)/;\n+\n+\t\tmy ($token, $value) = ($1, $2);\n+\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n+\t\t$q{$token} = $value;\n+\t\tlog_debug(\"We were given search token $token and value $value\");\n+\t}\n+\n+\tforeach (sort keys %q) {\n+\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n+\t}\n+\n+\treturn \\%q;\n+}\n+\n+# takes the search tokens and then a list of entries\n+# each entry is a hash reference\n+sub find_netrc_entry\n+{\n+\tmy $query = shift @_;\n+\n+    ENTRY:\n+\tforeach my $entry (@_)\n+\t{\n+\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n+\t\tforeach my $check (sort keys %$query) {\n+\t\t\tif (defined $query->{$check}) {\n+\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n+\t\t\t\t\t  $check,\n+\t\t\t\t\t  $entry->{$check},\n+\t\t\t\t\t  $query->{$check},\n+\t\t\t\t\t  $entry_text);\n+\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n+\t\t\t\t\tnext ENTRY;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn $entry;\n+\t}\n+\n+\t# nothing was found\n+\treturn;\n+}\n+\n+sub print_credential_data\n+{\n+\tmy $entry = shift @_;\n+\tmy $query = shift @_;\n+\n+\tlog_debug(\"entry has passed all the search checks\");\n+ TOKEN:\n+\tforeach my $git_token (sort keys %$entry) {\n+\t\tlog_debug(\"looking for useful token $git_token\");\n+\t\t# don't print unknown (to the credential helper protocol) tokens\n+\t\tnext TOKEN unless exists $query->{$git_token};\n+\n+\t\t# don't print things asked in the query (the entry matches them)\n+\t\tnext TOKEN if defined $query->{$git_token};\n+\n+\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n+\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n+\t}\n+}\n+sub log_verbose {\n+\treturn unless $options{verbose};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n+\n+sub log_debug {\n+\treturn unless $options{debug};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n-- \n1.7.9.rc2\n"},{"id":"208753","messageId":"7vip66sftf.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87k3qmr8yc.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T19:47:56Z","receivedAt":"2013-02-05T19:47:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> JCH> Oh, another thing. 'default' is like 'machine' followed by any\n> JCH> machine name, so the above while loop that reads two tokens\n> JCH> pair-wise needs to be aware that 'default' is not followed by a\n> JCH> value.  I think the loop will fail to parse this:\n>\n> JCH>         default       login anonymous    password me@home\n> JCH>         machine k.org login me           password mysecret\n>\n> I'd prefer to ignore \"default\" because it should not be used for the Git\n> credential helpers (its only use case is for anonymous services AFAIK).\n> So I'll add a case to ignore it in PATCHv4, if that's OK.\n\nYou still need to parse a file that has a \"default\" entry correctly;\notherwise the users won't be able to share existing .netrc files\nwith other applications e.g. ftp, which is the whole point of this\nseries.  Not using values from the \"default\" entry is probably fine,\nthough.\n\nThanks.\n"},{"id":"208754","messageId":"7vhalqsfkf.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87fw1ar3og.fsf_-_@lifelogs.com","subject":"Re: [PATCHv4] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T19:53:20Z","receivedAt":"2013-02-05T19:53:20Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> Changes since PATCHv3:\n>\n> - simple tests in Makefile\n> - support multiple files, code refactored\n> - documentation and comments updated\n> - fix IO::File for GPG pipe\n> - exit peacefully in almost every situation, die on bad invocation or query\n> - use log_verbose() and -v for logging for the user\n> - use log_debug() and -d for logging for the developer\n> - use Net::Netrc parser and `man netrc' to improve parsing\n> - ignore 'default' and 'macdef' netrc entries\n> - require 'machine' token in netrc lines\n> - ignore netrc files with bad permissions or owner (from Net::Netrc)\n\nPlease place the above _after_ the three-dashes.\n\nThe space here, above \"---\", is to justify why this change is a good\nidea to people who see this patch for the first time who never saw\nthe earlier rounds of this patch, e.g. reading \"git log\" output 6\nmonths down the road (see Documentation/SubmittingPatches \"(2)\nDescribe your changes well\").\n\n>\n> Signed-off-by: Ted Zlatanov <tzz@lifelogs.com>\n> ---\n>  contrib/credential/netrc/Makefile             |   10 +\n>  contrib/credential/netrc/git-credential-netrc |  423 +++++++++++++++++++++++++\n>  2 files changed, 433 insertions(+), 0 deletions(-)\n>  create mode 100644 contrib/credential/netrc/Makefile\n>  create mode 100755 contrib/credential/netrc/git-credential-netrc\n>\n> diff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\n> new file mode 100644\n> index 0000000..ee8c5f0\n> --- /dev/null\n> +++ b/contrib/credential/netrc/Makefile\n> @@ -0,0 +1,10 @@\n> +test_netrc:\n> +\t@(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring failure\"\n> +\t@echo \"-> Silent invocation... nothing should show up here with a missing file\"\n\nAvoid starting an argument to \"echo\" with a dash; some\nimplementations choke with \"unknown option\".\n\n> +\t@echo \"bad data\" | ./git-credential-netrc -f A get\n> +\t@echo \"-> Back to noisy: -v and -d used below, missing file\"\n> +\techo \"bad data\" | ./git-credential-netrc -f A -d -v get\n> +\t@echo \"-> Look for any entry in the default file set\"\n> +\techo \"\" | ./git-credential-netrc -d -v get\n> +\t@echo \"-> Look for github.com in the default file set\"\n> +\techo \"host=google.com\" | ./git-credential-netrc -d -v get\n> diff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\n> new file mode 100755\n> index 0000000..6946217\n> --- /dev/null\n> +++ b/contrib/credential/netrc/git-credential-netrc\n> @@ -0,0 +1,423 @@\n> +#!/usr/bin/perl\n> +\n> +use strict;\n> +use warnings;\n> +\n> +use Getopt::Long;\n> +use File::Basename;\n> +\n> +my $VERSION = \"0.1\";\n> +\n> +my %options = (\n> +               help => 0,\n> +               debug => 0,\n> +               verbose => 0,\n> +\t       file => [],\n\nLooks like there is some funny indentation going on here.\n\n> +\n> +               # identical token maps, e.g. host -> host, will be inserted later\n> +               tmap => {\n> +                        port => 'protocol',\n> +                        machine => 'host',\n> +                        path => 'path',\n> +                        login => 'username',\n> +                        user => 'username',\n> +                        password => 'password',\n> +                       }\n> +              );\n> +\n> +# Map each credential protocol token to itself on the netrc side.\n> +foreach (values %{$options{tmap}}) {\n> +\t$options{tmap}->{$_} = $_;\n> +}\n> +\n> +# Now, $options{tmap} has a mapping from the netrc format to the Git credential\n> +# helper protocol.\n> +\n> +# Next, we build the reverse token map.\n> +\n> +# When $rmap{foo} contains 'bar', that means that what the Git credential helper\n> +# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in\n> +# %rmap are what we expect to read from the netrc/authinfo file.\n> +\n> +my %rmap;\n> +foreach my $k (keys %{$options{tmap}}) {\n> +\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n> +}\n> +\n> +Getopt::Long::Configure(\"bundling\");\n> +\n> +# TODO: maybe allow the token map $options{tmap} to be configurable.\n> +GetOptions(\\%options,\n> +           \"help|h\",\n> +           \"debug|d\",\n> +           \"verbose|v\",\n> +           \"file|f=s@\",\n> +          );\n> +\n> +if ($options{help}) {\n> +\tmy $shortname = basename($0);\n> +\t$shortname =~ s/git-credential-//;\n> +\n> +\tprint <<EOHIPPUS;\n> +\n> +$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] get\n> +\n> +Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n> +\n> +Options:\n> +\n> +  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n> +                       will be decrypted by GPG before parsing.  Multiple -f\n> +                       arguments are OK, and the order is respected.\n\nSaying \"order is respected\" without mentioning the collision\nresolution rules is not helpful to the users when deciding in what\norder they should give these files.  First one wins, or last one\nwins?  Later you say \"looks for the first entry\", but it will be\nmuch easier to read the above to mention it here as well.\n\n> +  -d|--debug         : turn on debugging (developer info)\n> +\n> +  -v|--verbose       : be more verbose (show files and information found)\n> +\n> +To enable this credential helper:\n> +\n> +  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'\n> +\n> +(Note that Git will prepend \"git-credential-\" to the helper name and look for it\n> +in the path.)\n> +\n> +...and if you want lots of debugging info:\n> +\n> +  git config credential.helper '$shortname -f AUTHFILE -d'\n> +\n> +...or to see the files opened and data found:\n> +\n> +  git config credential.helper '$shortname -f AUTHFILE -v'\n> +\n> +Only \"get\" mode is supported by this credential helper.  It opens every AUTHFILE\n> +and looks for the first entry that matches the requested search criteria:\n> +\n> + 'port|protocol':\n> +   The protocol that will be used (e.g., https). (protocol=X)\n> +\n> + 'machine|host':\n> +   The remote hostname for a network credential. (host=X)\n> +\n> + 'path':\n> +   The path with which the credential will be used. (path=X)\n> +\n> + 'login|user|username':\n> +   The credential’s username, if we already have one. (username=X)\n> +\n> +Thus, when we get this query on STDIN:\n> +\n> +protocol=https\n> +username=tzz\n> +\n> +this credential helper will look for the first entry in every AUTHFILE that\n> +matches\n> +\n> +port https login tzz\n> +\n> +OR\n> +\n> +protocol https login tzz\n> +\n> +OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n> +simply ignored.\n> +\n> +Then, the helper will print out whatever tokens it got from the entry, including\n> +\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n> +back to \"protocol\".\n\nIsn't \"hostname\" typically what users expect to see?  It is somewhat\nunnerving to see an example that throws the same password back to\nany host you happen to have an accoutn \"tzz\" on, even though that is\nnot technically an invalid way to use this helper.\n\n> +Again, note that the first matching entry from all the AUTHFILEs is used.\n> +\n> +Tokens can be quoted as 'STRING' or \"STRING\".\n> +\n> +No caching is performed by this credential helper.\n> +\n> +EOHIPPUS\n\nOtherwise, nice write-up.\n\n> +my $mode = shift @ARGV;\n> +\n> +# Credentials must get a parameter, so die if it's missing.\n> +die \"Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get\" unless defined $mode;\n> +\n> +# Only support 'get' mode; with any other unsupported ones we just exit.\n> +exit 0 unless $mode eq 'get';\n> +\n> +my $files = $options{file};\n> +\n> +# if no files were given, use a predefined list.\n> +# note that .gpg files come first\n> +unless (scalar @$files)\n> +{\n> +\tmy @candidates = qw[\n> +\t\t\t\t   ~/.authinfo.gpg\n> +\t\t\t\t   ~/.netrc.gpg\n> +\t\t\t\t   ~/.authinfo\n> +\t\t\t\t   ~/.netrc\n> +\t\t\t  ];\n> +\n> +\t$files = $options{file} = [ map { glob $_ } @candidates ];\n> +}\n> +\n> +my $query = read_credential_data_from_stdin();\n> +\n> +FILE:\n> +foreach my $file (@$files)\n> +{\n> +\tunless (-r $file)\n> +\t{\n> +\t\tlog_verbose(\"Unable to read $file; skipping it\");\n> +\t\tnext FILE;\n> +\t}\n> +\n> +\t# the following check is copied from Net::Netrc\n> +\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n> +\tunless ($^O eq 'os2'\n> +\t\t|| $^O eq 'MSWin32'\n> +\t\t|| $^O eq 'MacOS'\n> +\t\t|| $^O =~ /^cygwin/)\n> +\t{\n> +\t\tmy @stat = stat($file);\n> +\n> +\t\tif (@stat) {\n> +\t\t\tif ($stat[2] & 077) {\n> +\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n> +\t\t\t\t\t    $stat[2] & 07777);\n\nNice touch, although I am not sure rejecting world or group readable\nencrypted file is absolutely necessary.\n\n> +\t\t\t\tnext FILE;\n> +\t\t\t}\n> +\t\t\tif ($stat[4] != $<) {\n> +\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n> +\t\t\t\tnext FILE;\n\nOK.  A group of local users may share the same account at the\nremote, but that would be unusual.\n\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +\tmy $mode = (stat($file))[2];\n> +\tif ($mode & 077)\n> +\t{\n> +\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n> +\t\t\t    $mode & 07777);\n\nAgain?  Didn't you just do this?\n\n> +\t\tnext FILE;\n> +\t}\n> +\n> +\tmy @entries = load_netrc($file);\n> +\n> +\tunless (scalar @entries)\n> +\t{\n> +\t\tif ($!)\n> +\t\t{\n> +\t\t\tlog_verbose(\"Unable to open $file: $!\");\n> +\t\t}\n> +\t\telse\n> +\t\t{\n> +\t\t\tlog_verbose(\"No netrc entries found in $file\");\n> +\t\t}\n\nI think the prevalent style is to\n\n\tif (condition) {\n        \tdo this;\n\t} elsif (another condition) {\n\t\tdo that\n\t} else {\n\t\tdo that other thing;\n\t}\n\n(this comment applies to all if/elsif/else cascades in this patch).\n\n> +\n> +\t\tnext FILE;\n\nIsn't this outermost loop, by the way?  What the motivation to have\nan explicit label everywhere (not complaining---it could be your own\ndiscipline thing---just wondering).\n\n> +\t}\n> +\n> +\tmy $entry = find_netrc_entry($query, @entries);\n> +\tif ($entry)\n> +\t{\n> +\t\tprint_credential_data($entry, $query);\n> +\t\t# we're done!\n> +\t\tlast FILE;\n> +\t}\n> +}\n> +\n> +exit 0;\n> +\n> +sub load_netrc\n> +{\n> +\tmy $file = shift @_;\n> +\n> +\tmy $io;\n> +\tif ($file =~ m/\\.gpg$/) {\n> +\t\tlog_verbose(\"Using GPG to open $file\");\n> +\t\t# GPG doesn't work well with 2- or 3-argument open\n\nIf that is the case, please quote $file properly against shell\nmunging it.\n\nThe only thing you do on $io is to read from it via \"while (<$io>)\",\nso I would personally have written this part like this without\nhaving to use IO::File(), though:\n\n\t$io = open(\"-|\", qw(gpg --decrypt), $file);\n\nSimilarly for the plain file:\n\n\t$io = open(\"<\", $file);\n\n> +\t\t$io = new IO::File(\"gpg --decrypt $file|\");\n> +\t}\n> +\telse {\n> +\t\tlog_verbose(\"Opening $file...\");\n> +\t\t$io = new IO::File($file, '<');\n> +\t}\n> +\n> +\t# nothing to do if the open failed (we log the error later)\n> +\treturn unless $io;\n> +\n> +\t# Net::Netrc does this, but the functionality is merged with the file\n> +\t# detection logic, so we have to extract just the part we need\n> +\tmy @netrc_entries = net_netrc_loader($io);\n> +\n> +\t# these entries will use the credential helper protocol token names\n> +\tmy @entries;\n> +\n> +\tforeach my $nentry (@netrc_entries) {\n> +\t\tmy %entry;\n> +\t\tmy $num_port;\n> +\n> +\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n> +\t\t\t$num_port = $nentry->{port};\n> +\t\t\tdelete $nentry->{port};\n> +\t\t}\n> +\n> +\t\t# create the new entry for the credential helper protocol\n> +\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n> +\n> +\t\t# for \"host X port Y\" where Y is an integer (captured by\n> +\t\t# $num_port above), set the host to \"X:Y\"\n> +\t\tif (defined $entry{host} && defined $num_port) {\n> +\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n> +\t\t}\n> +\n> +\t\tpush @entries, \\%entry;\n> +\t}\n> +\n> +\treturn @entries;\n> +}\n> +\n> +sub net_netrc_loader\n> +{\n> +\tmy $fh = shift @_;\n> +\tmy @entries;\n> +\tmy ($mach, $macdef, $tok, @tok) = (0, 0);\n\nI think you meant to use $mach as a reference to a hash and $macdef\nas a reference to an array; do you want to initialize them to\nnumeric zeros?\n\n(The remainder of the patch unsnipped for others' reference).\n\nThanks.\n\n> +    LINE:\n> +\twhile (<$fh>) {\n> +\t\tundef $macdef if /\\A\\n\\Z/;\n> +\n> +\t\tif ($macdef) {\n> +\t\t\tpush(@$macdef, $_);\n> +\t\t\tnext LINE;\n> +\t\t}\n> +\n> +\t\ts/^\\s*//;\n> +\t\tchomp;\n> +\n> +\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n> +\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n> +\t\t\tpush(@tok, $tok);\n> +\t\t}\n> +\n> +\t    TOKEN:\n> +\t\twhile (@tok) {\n> +\t\t\t$tok = shift(@tok);\n> +\n> +\t\t\tif ($tok eq \"machine\") {\n> +\t\t\t\tmy $host = shift @tok;\n> +\t\t\t\t$mach = { machine => $host };\n> +\t\t\t\tpush @entries, $mach;\n> +\t\t\t}\n> +\t\t\telsif (exists $options{tmap}->{$tok}) {\n> +\t\t\t\tunless ($mach) {\n> +\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n> +\t\t\t\t\tnext TOKEN;\n> +\t\t\t\t}\n> +\n> +\t\t\t\tmy $value = shift @tok;\n> +\t\t\t\tunless (defined $value) {\n> +\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n> +\t\t\t\t\tnext TOKEN;\n> +\t\t\t\t}\n> +\n> +\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n> +\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n> +\t\t\t\t$mach->{$tok} = $value;\n> +\t\t\t}\n> +\t\t\telsif ($tok eq \"macdef\") { # we ignore macros\n> +\t\t\t\tnext TOKEN unless $mach;\n> +\t\t\t\tmy $value = shift @tok;\n> +\t\t\t\t$mach->{macdef} = {} unless exists $mach->{macdef};\n> +\t\t\t\t$macdef = $mach->{machdef}{$value} = [];\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +\treturn @entries;\n> +}\n> +\n> +sub read_credential_data_from_stdin\n> +{\n> +\t# the query: start with every token with no value\n> +\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n> +\n> +\twhile (<STDIN>) {\n> +\t\tnext unless m/^([^=]+)=(.+)/;\n> +\n> +\t\tmy ($token, $value) = ($1, $2);\n> +\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n> +\t\t$q{$token} = $value;\n> +\t\tlog_debug(\"We were given search token $token and value $value\");\n> +\t}\n> +\n> +\tforeach (sort keys %q) {\n> +\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n> +\t}\n> +\n> +\treturn \\%q;\n> +}\n> +\n> +# takes the search tokens and then a list of entries\n> +# each entry is a hash reference\n> +sub find_netrc_entry\n> +{\n> +\tmy $query = shift @_;\n> +\n> +    ENTRY:\n> +\tforeach my $entry (@_)\n> +\t{\n> +\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n> +\t\tforeach my $check (sort keys %$query) {\n> +\t\t\tif (defined $query->{$check}) {\n> +\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n> +\t\t\t\t\t  $check,\n> +\t\t\t\t\t  $entry->{$check},\n> +\t\t\t\t\t  $query->{$check},\n> +\t\t\t\t\t  $entry_text);\n> +\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n> +\t\t\t\t\tnext ENTRY;\n> +\t\t\t\t}\n> +\t\t\t}\n> +\t\t\telse {\n> +\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\treturn $entry;\n> +\t}\n> +\n> +\t# nothing was found\n> +\treturn;\n> +}\n> +\n> +sub print_credential_data\n> +{\n> +\tmy $entry = shift @_;\n> +\tmy $query = shift @_;\n> +\n> +\tlog_debug(\"entry has passed all the search checks\");\n> + TOKEN:\n> +\tforeach my $git_token (sort keys %$entry) {\n> +\t\tlog_debug(\"looking for useful token $git_token\");\n> +\t\t# don't print unknown (to the credential helper protocol) tokens\n> +\t\tnext TOKEN unless exists $query->{$git_token};\n> +\n> +\t\t# don't print things asked in the query (the entry matches them)\n> +\t\tnext TOKEN if defined $query->{$git_token};\n> +\n> +\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n> +\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n> +\t}\n> +}\n> +sub log_verbose {\n> +\treturn unless $options{verbose};\n> +\tprintf STDERR @_;\n> +\tprintf STDERR \"\\n\";\n> +}\n> +\n> +sub log_debug {\n> +\treturn unless $options{debug};\n> +\tprintf STDERR @_;\n> +\tprintf STDERR \"\\n\";\n> +}\n"},{"id":"208755","messageId":"87bobyr0ju.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vip66sftf.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T20:03:01Z","receivedAt":"2013-02-05T20:03:01Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 11:47:56 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\nJCH> Oh, another thing. 'default' is like 'machine' followed by any\nJCH> machine name, so the above while loop that reads two tokens\nJCH> pair-wise needs to be aware that 'default' is not followed by a\nJCH> value.  I think the loop will fail to parse this:\n>> \nJCH> default       login anonymous    password me@home\nJCH> machine k.org login me           password mysecret\n>> \n>> I'd prefer to ignore \"default\" because it should not be used for the Git\n>> credential helpers (its only use case is for anonymous services AFAIK).\n>> So I'll add a case to ignore it in PATCHv4, if that's OK.\n\nJCH> You still need to parse a file that has a \"default\" entry correctly;\nJCH> otherwise the users won't be able to share existing .netrc files\nJCH> with other applications e.g. ftp, which is the whole point of this\nJCH> series.  Not using values from the \"default\" entry is probably fine,\nJCH> though.\n\nOK; done in PATCHv4.\n\nTed\n"},{"id":"208758","messageId":"7vd2wese6z.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87bobyr0ju.fsf@lifelogs.com","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T20:23:00Z","receivedAt":"2013-02-05T20:23:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> JCH> You still need to parse a file that has a \"default\" entry correctly;\n> JCH> otherwise the users won't be able to share existing .netrc files\n> JCH> with other applications e.g. ftp, which is the whole point of this\n> JCH> series.  Not using values from the \"default\" entry is probably fine,\n> JCH> though.\n>\n> OK; done in PATCHv4.\n\nHmph.\n\nDidn't you remove that from your version of net_netrc_loader when\nyou borrowed the bulk of the code from Net::Netrc::_readrc?  I see\n\"default\" token handled at the beginning of \"TOKEN: while (@tok)\"\nloop in the original but not in your version I see in v4.\n"},{"id":"208761","messageId":"877gmmqyho.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vhalqsfkf.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv4] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T20:47:31Z","receivedAt":"2013-02-05T20:47:31Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 11:53:20 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\n>> Changes since PATCHv3:\n>> \n>> - simple tests in Makefile\n>> - support multiple files, code refactored\n>> - documentation and comments updated\n>> - fix IO::File for GPG pipe\n>> - exit peacefully in almost every situation, die on bad invocation or query\n>> - use log_verbose() and -v for logging for the user\n>> - use log_debug() and -d for logging for the developer\n>> - use Net::Netrc parser and `man netrc' to improve parsing\n>> - ignore 'default' and 'macdef' netrc entries\n>> - require 'machine' token in netrc lines\n>> - ignore netrc files with bad permissions or owner (from Net::Netrc)\n\nJCH> Please place the above _after_ the three-dashes.\n\nJCH> The space here, above \"---\", is to justify why this change is a good\nJCH> idea to people who see this patch for the first time who never saw\nJCH> the earlier rounds of this patch, e.g. reading \"git log\" output 6\nJCH> months down the road (see Documentation/SubmittingPatches \"(2)\nJCH> Describe your changes well\").\n\nWill do in PATCHv5.\n\nJCH> Avoid starting an argument to \"echo\" with a dash; some\nJCH> implementations choke with \"unknown option\".\n\nNice, thanks.  It's purely decorative so I use '=>' now.\n\n>> +my %options = (\n>> +               help => 0,\n>> +               debug => 0,\n>> +               verbose => 0,\n>> +\t       file => [],\n\nJCH> Looks like there is some funny indentation going on here.\n\nFixed.\n\n>> +  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n>> +                       will be decrypted by GPG before parsing.  Multiple -f\n>> +                       arguments are OK, and the order is respected.\n\nJCH> Saying \"order is respected\" without mentioning the collision\nJCH> resolution rules is not helpful to the users when deciding in what\nJCH> order they should give these files.  First one wins, or last one\nJCH> wins?  Later you say \"looks for the first entry\", but it will be\nJCH> much easier to read the above to mention it here as well.\n\nRight.  Reworded.\n\n>> +Thus, when we get this query on STDIN:\n>> +\n>> +protocol=https\n>> +username=tzz\n>> +\n>> +this credential helper will look for the first entry in every AUTHFILE that\n>> +matches\n>> +\n>> +port https login tzz\n>> +\n>> +OR\n>> +\n>> +protocol https login tzz\n>> +\n>> +OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n>> +simply ignored.\n>> +\n>> +Then, the helper will print out whatever tokens it got from the entry, including\n>> +\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n>> +back to \"protocol\".\n\nJCH> Isn't \"hostname\" typically what users expect to see?  It is somewhat\nJCH> unnerving to see an example that throws the same password back to\nJCH> any host you happen to have an accoutn \"tzz\" on, even though that is\nJCH> not technically an invalid way to use this helper.\n\nYeah, I changed it to show \"machine\" in the query (which would be more typical).\n\n>> +\t\t\tif ($stat[2] & 077) {\n>> +\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n>> +\t\t\t\t\t    $stat[2] & 07777);\n\nJCH> Nice touch, although I am not sure rejecting world or group readable\nJCH> encrypted file is absolutely necessary.\n\nRight.  Fixed.\n\n>> +\t\t\tif ($stat[4] != $<) {\n>> +\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n>> +\t\t\t\tnext FILE;\n\nJCH> OK.  A group of local users may share the same account at the\nJCH> remote, but that would be unusual.\n\nI added --insecure/-k to override this check.\n\n>> +\tif ($mode & 077)\n\nJCH> Again?  Didn't you just do this?\n\nDamn, sorry.\n\nJCH> I think the prevalent style is to\n\nJCH> \tif (condition) {\nJCH>         \tdo this;\nJCH> \t} elsif (another condition) {\nJCH> \t\tdo that\nJCH> \t} else {\nJCH> \t\tdo that other thing;\nJCH> \t}\n\nJCH> (this comment applies to all if/elsif/else cascades in this patch).\n\nYup.  I was working with Net::Netrc code and forgot to reformat it.  Sorry.\n\n>> +\n>> +\t\tnext FILE;\n\nJCH> Isn't this outermost loop, by the way?  What the motivation to have\nJCH> an explicit label everywhere (not complaining---it could be your own\nJCH> discipline thing---just wondering).\n\nI think it's more readable with large loops, and it actually makes sense\nwhen you read the code.  Not a big deal to me either, I just felt for\nthis particular script it was OK.\n\n>> +\tif ($file =~ m/\\.gpg$/) {\n>> +\t\tlog_verbose(\"Using GPG to open $file\");\n>> +\t\t# GPG doesn't work well with 2- or 3-argument open\n\nJCH> If that is the case, please quote $file properly against shell\nJCH> munging it.\n\nAhhh that gets ugly.  OK, quoted.\n\nJCH> The only thing you do on $io is to read from it via \"while (<$io>)\",\nJCH> so I would personally have written this part like this without\nJCH> having to use IO::File(), though:\n\nJCH> \t$io = open(\"-|\", qw(gpg --decrypt), $file);\n\nThat doesn't work for me, unfortunately.  I'm trying to avoid the IPC::*\nmodules and such.  Please test it yourself with GPG.  I'm on Perl\n5.14.2.\n\nI think it's OK with the quoting, as you'll see in PATCHv5.\n\nJCH> Similarly for the plain file:\n\nJCH> \t$io = open(\"<\", $file);\n\nYou mean \"open $io, '<', $file\".  open() returns nonzero on success and\nundef on failure.  OK, I'll use open() instead of IO::File.\n\n>> +\tmy ($mach, $macdef, $tok, @tok) = (0, 0);\n\nJCH> I think you meant to use $mach as a reference to a hash and $macdef\nJCH> as a reference to an array; do you want to initialize them to\nJCH> numeric zeros?\n\nThat actually came from Net::Netrc.  The $mach default is OK either way;\nI left it undefined so it's clearer. I think the $macdef initial value\nis a bug (which, I guess, shows macros are very rare); I just made it a\nboolean for our purposes.\n\nTed\n"},{"id":"208764","messageId":"8738xaqy40.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"7vhalqsfkf.fsf@alter.siamese.dyndns.org","subject":"[PATCHv5] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T20:55:43Z","receivedAt":"2013-02-05T20:55:43Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"Add Git credential helper that can parse netrc/authinfo files.\n\nThis credential helper support multiple files, returning the first one\nthat matches.  It checks file permissions and owner.  For *.gpg files,\nit will run GPG to decrypt the file.\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\nChanges since PATCHv4:\n\n- indentation and brace fixes\n- test makefile uses \"=>\" as the decorative prefix\n- documentation fixes about order and show query with \"hostname\"\n- add --insecure to ignore owner and permission checks\n- check permissions just once and only for unencrypted files\n- change IO::File to simple open() and quote $file\n- fixed macdef buglet from Net::Netrc\n- ignore 'default' entries\n\n contrib/credential/netrc/Makefile             |   12 +\n contrib/credential/netrc/git-credential-netrc |  424 +++++++++++++++++++++++++\n 2 files changed, 436 insertions(+), 0 deletions(-)\n create mode 100644 contrib/credential/netrc/Makefile\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\nnew file mode 100644\nindex 0000000..18a924f\n--- /dev/null\n+++ b/contrib/credential/netrc/Makefile\n@@ -0,0 +1,12 @@\n+test_netrc:\n+\t@(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring failure\"\n+\t@echo \"=> Silent invocation... nothing should show up here with a missing file\"\n+\t@echo \"bad data\" | ./git-credential-netrc -f A get\n+\t@echo \"=> Back to noisy: -v and -d used below, missing file\"\n+\techo \"bad data\" | ./git-credential-netrc -f A -d -v get\n+\t@echo \"=> Look for any entry in the default file set\"\n+\techo \"\" | ./git-credential-netrc -d -v get\n+\t@echo \"=> Look for github.com in the default file set\"\n+\techo \"host=google.com\" | ./git-credential-netrc -d -v get\n+\t@echo \"=> Look for a nonexistent machine in the default file set\"\n+\techo \"host=korovamilkbar\" | ./git-credential-netrc -d -v get\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..8298564\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,424 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+\t       help => 0,\n+\t       debug => 0,\n+\t       verbose => 0,\n+\t       insecure => 0,\n+\t       file => [],\n+\n+\t       # identical token maps, e.g. host -> host, will be inserted later\n+\t       tmap => {\n+\t\t\tport => 'protocol',\n+\t\t\tmachine => 'host',\n+\t\t\tpath => 'path',\n+\t\t\tlogin => 'username',\n+\t\t\tuser => 'username',\n+\t\t\tpassword => 'password',\n+\t\t       }\n+\t      );\n+\n+# Map each credential protocol token to itself on the netrc side.\n+foreach (values %{$options{tmap}}) {\n+\t$options{tmap}->{$_} = $_;\n+}\n+\n+# Now, $options{tmap} has a mapping from the netrc format to the Git credential\n+# helper protocol.\n+\n+# Next, we build the reverse token map.\n+\n+# When $rmap{foo} contains 'bar', that means that what the Git credential helper\n+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in\n+# %rmap are what we expect to read from the netrc/authinfo file.\n+\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"insecure|k\",\n+           \"verbose|v\",\n+           \"file|f=s@\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+\n+  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n+                       will be decrypted by GPG before parsing.  Multiple -f\n+                       arguments are OK.  They are processed in order, and the\n+                       first matching entry found is returned via the credential\n+                       helper protocol (see below).\n+\n+  -k|--insecure      : ignore bad file ownership or permissions\n+\n+  -d|--debug         : turn on debugging (developer info)\n+\n+  -v|--verbose       : be more verbose (show files and information found)\n+\n+To enable this credential helper:\n+\n+  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'\n+\n+(Note that Git will prepend \"git-credential-\" to the helper name and look for it\n+in the path.)\n+\n+...and if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+...or to see the files opened and data found:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -v'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens every AUTHFILE\n+and looks for the first entry that matches the requested search criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+host=github.com\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for the first entry in every AUTHFILE that\n+matches\n+\n+machine github.com port https login tzz\n+\n+OR\n+\n+machine github.com protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the entry, including\n+\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n+back to \"protocol\".  Any redundant entry tokens (part of the original query) are\n+skipped.\n+\n+Again, note that only the first matching entry from all the AUTHFILEs, processed\n+in the sequence given on the command line, is used.\n+\n+Netrc/authinfo tokens can be quoted as 'STRING' or \"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit 0;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# Credentials must get a parameter, so die if it's missing.\n+die \"Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get\" unless defined $mode;\n+\n+# Only support 'get' mode; with any other unsupported ones we just exit.\n+exit 0 unless $mode eq 'get';\n+\n+my $files = $options{file};\n+\n+# if no files were given, use a predefined list.\n+# note that .gpg files come first\n+unless (scalar @$files) {\n+\tmy @candidates = qw[\n+\t\t\t\t   ~/.authinfo.gpg\n+\t\t\t\t   ~/.netrc.gpg\n+\t\t\t\t   ~/.authinfo\n+\t\t\t\t   ~/.netrc\n+\t\t\t  ];\n+\n+\t$files = $options{file} = [ map { glob $_ } @candidates ];\n+}\n+\n+my $query = read_credential_data_from_stdin();\n+\n+FILE:\n+foreach my $file (@$files) {\n+\tmy $gpgmode = $file =~ m/\\.gpg$/;\n+\tunless (-r $file) {\n+\t\tlog_verbose(\"Unable to read $file; skipping it\");\n+\t\tnext FILE;\n+\t}\n+\n+\t# the following check is copied from Net::Netrc, for non-GPG files\n+\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n+\tunless ($gpgmode || $options{insecure} ||\n+\t\t$^O eq 'os2'\n+\t\t|| $^O eq 'MSWin32'\n+\t\t|| $^O eq 'MacOS'\n+\t\t|| $^O =~ /^cygwin/) {\n+\t\tmy @stat = stat($file);\n+\n+\t\tif (@stat) {\n+\t\t\tif ($stat[2] & 077) {\n+\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n+\t\t\t\t\t    $stat[2] & 07777);\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\n+\t\t\tif ($stat[4] != $<) {\n+\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tmy @entries = load_netrc($file, $gpgmode);\n+\n+\tunless (scalar @entries) {\n+\t\tif ($!) {\n+\t\t\tlog_verbose(\"Unable to open $file: $!\");\n+\t\t}\n+\t\telse {\n+\t\t\tlog_verbose(\"No netrc entries found in $file\");\n+\t\t}\n+\n+\t\tnext FILE;\n+\t}\n+\n+\tmy $entry = find_netrc_entry($query, @entries);\n+\tif ($entry) {\n+\t\tprint_credential_data($entry, $query);\n+\t\t# we're done!\n+\t\tlast FILE;\n+\t}\n+}\n+\n+exit 0;\n+\n+sub load_netrc {\n+\tmy $file = shift @_;\n+\tmy $gpgmode = shift @_;\n+\n+\tmy $io;\n+\tif ($gpgmode) {\n+\t\t# typical shell character escapes from http://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html\n+\t\tmy $f = $file;\n+\t\t$f =~ s/([;<>\\*\\|`&\\$!#\\(\\)\\[\\]\\{\\}:'\"])/\\\\$1/g;\n+\t\t# GPG doesn't work well with 2- or 3-argument open\n+\t\tmy $cmd = \"gpg --decrypt $f\";\n+\t\tlog_verbose(\"Using GPG to open $file: [$cmd]\");\n+\t\topen $io, \"$cmd|\";\n+\t}\n+\telse {\n+\t\tlog_verbose(\"Opening $file...\");\n+\t\topen $io, '<', $file;\n+\t}\n+\n+\t# nothing to do if the open failed (we log the error later)\n+\treturn unless $io;\n+\n+\t# Net::Netrc does this, but the functionality is merged with the file\n+\t# detection logic, so we have to extract just the part we need\n+\tmy @netrc_entries = net_netrc_loader($io);\n+\n+\t# these entries will use the credential helper protocol token names\n+\tmy @entries;\n+\n+\tforeach my $nentry (@netrc_entries) {\n+\t\tmy %entry;\n+\t\tmy $num_port;\n+\n+\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n+\t\t\t$num_port = $nentry->{port};\n+\t\t\tdelete $nentry->{port};\n+\t\t}\n+\n+\t\t# create the new entry for the credential helper protocol\n+\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n+\n+\t\t# for \"host X port Y\" where Y is an integer (captured by\n+\t\t# $num_port above), set the host to \"X:Y\"\n+\t\tif (defined $entry{host} && defined $num_port) {\n+\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n+\t\t}\n+\n+\t\tpush @entries, \\%entry;\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub net_netrc_loader {\n+\tmy $fh = shift @_;\n+\tmy @entries;\n+\tmy ($mach, $macdef, $tok, @tok);\n+\n+    LINE:\n+\twhile (<$fh>) {\n+\t\tundef $macdef if /\\A\\n\\Z/;\n+\n+\t\tif ($macdef) {\n+\t\t\tnext LINE;\n+\t\t}\n+\n+\t\ts/^\\s*//;\n+\t\tchomp;\n+\n+\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\t\tpush(@tok, $tok);\n+\t\t}\n+\n+\t    TOKEN:\n+\t\twhile (@tok) {\n+\t\t\tif ($tok[0] eq \"default\") {\n+\t\t\t\tshift(@tok);\n+\t\t\t\tundef $mach; # ignore 'default' lines\n+\n+\t\t\t\tnext TOKEN;\n+\t\t\t}\n+\n+\t\t\t$tok = shift(@tok);\n+\n+\t\t\tif ($tok eq \"machine\") {\n+\t\t\t\tmy $host = shift @tok;\n+\t\t\t\t$mach = { machine => $host };\n+\t\t\t\tpush @entries, $mach;\n+\t\t\t}\n+\t\t\telsif (exists $options{tmap}->{$tok}) {\n+\t\t\t\tunless ($mach) {\n+\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\tunless (defined $value) {\n+\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n+\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n+\t\t\t\t$mach->{$tok} = $value;\n+\t\t\t}\n+\t\t\telsif ($tok eq \"macdef\") { # we ignore macros\n+\t\t\t\tnext TOKEN unless $mach;\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\t$macdef = 1;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub read_credential_data_from_stdin {\n+\t# the query: start with every token with no value\n+\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+\twhile (<STDIN>) {\n+\t\tnext unless m/^([^=]+)=(.+)/;\n+\n+\t\tmy ($token, $value) = ($1, $2);\n+\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n+\t\t$q{$token} = $value;\n+\t\tlog_debug(\"We were given search token $token and value $value\");\n+\t}\n+\n+\tforeach (sort keys %q) {\n+\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n+\t}\n+\n+\treturn \\%q;\n+}\n+\n+# takes the search tokens and then a list of entries\n+# each entry is a hash reference\n+sub find_netrc_entry {\n+\tmy $query = shift @_;\n+\n+    ENTRY:\n+\tforeach my $entry (@_)\n+\t{\n+\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n+\t\tforeach my $check (sort keys %$query) {\n+\t\t\tif (defined $query->{$check}) {\n+\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n+\t\t\t\t\t  $check,\n+\t\t\t\t\t  $entry->{$check},\n+\t\t\t\t\t  $query->{$check},\n+\t\t\t\t\t  $entry_text);\n+\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n+\t\t\t\t\tnext ENTRY;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn $entry;\n+\t}\n+\n+\t# nothing was found\n+\treturn;\n+}\n+\n+sub print_credential_data {\n+\tmy $entry = shift @_;\n+\tmy $query = shift @_;\n+\n+\tlog_debug(\"entry has passed all the search checks\");\n+ TOKEN:\n+\tforeach my $git_token (sort keys %$entry) {\n+\t\tlog_debug(\"looking for useful token $git_token\");\n+\t\t# don't print unknown (to the credential helper protocol) tokens\n+\t\tnext TOKEN unless exists $query->{$git_token};\n+\n+\t\t# don't print things asked in the query (the entry matches them)\n+\t\tnext TOKEN if defined $query->{$git_token};\n+\n+\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n+\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n+\t}\n+}\n+sub log_verbose {\n+\treturn unless $options{verbose};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n+\n+sub log_debug {\n+\treturn unless $options{debug};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n-- \n1.7.9.rc2\n"},{"id":"208765","messageId":"87y5f2pjay.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vd2wese6z.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCH] Add contrib/credentials/netrc with GPG support, try #2","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T21:00:53Z","receivedAt":"2013-02-05T21:00:53Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 12:23:00 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\nJCH> You still need to parse a file that has a \"default\" entry correctly;\nJCH> otherwise the users won't be able to share existing .netrc files\nJCH> with other applications e.g. ftp, which is the whole point of this\nJCH> series.  Not using values from the \"default\" entry is probably fine,\nJCH> though.\n>> \n>> OK; done in PATCHv4.\n\nJCH> Hmph.\n\nJCH> Didn't you remove that from your version of net_netrc_loader when\nJCH> you borrowed the bulk of the code from Net::Netrc::_readrc?  I see\nJCH> \"default\" token handled at the beginning of \"TOKEN: while (@tok)\"\nJCH> loop in the original but not in your version I see in v4.\n\nDamn, I accidentally moved that check to /dev/null.  OK, I added it in\nPATCHv5.  Thanks for catching that.\n\nTed\n"},{"id":"208771","messageId":"7vpq0equo9.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"877gmmqyho.fsf@lifelogs.com","subject":"Re: [PATCHv4] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T22:09:58Z","receivedAt":"2013-02-05T22:09:58Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> On Tue, 05 Feb 2013 11:53:20 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n>\n> I think it's more readable with large loops, and it actually makes sense\n> when you read the code.  Not a big deal to me either, I just felt for\n> this particular script it was OK.\n>\n>>> +\tif ($file =~ m/\\.gpg$/) {\n>>> +\t\tlog_verbose(\"Using GPG to open $file\");\n>>> +\t\t# GPG doesn't work well with 2- or 3-argument open\n>\n> JCH> If that is the case, please quote $file properly against shell\n> JCH> munging it.\n>\n> Ahhh that gets ugly.  OK, quoted.\n>\n> JCH> The only thing you do on $io is to read from it via \"while (<$io>)\",\n> JCH> so I would personally have written this part like this without\n> JCH> having to use IO::File(), though:\n>\n> JCH> \t$io = open(\"-|\", qw(gpg --decrypt), $file);\n>\n> That doesn't work for me, unfortunately.  I'm trying to avoid the IPC::*\n> modules and such.  Please test it yourself with GPG.  I'm on Perl\n> 5.14.2.\n\nThis works for me as expected (sorry for that open $io syntax\ngotcha).\n\n-- cut here -- >8 -- cut here --\n\n#!/usr/bin/perl\nmy $io;\nopen $io, \"-|\", qw(gpg --decrypt), $ARGV[0]\n        or die \"$!: gpg open\";\nwhile (<$io>) {\n        print;\n}\nclose $io\n        or die \"$!: gpg close\";\n\n-- cut here -- 8< -- cut here --\n\n$ perl --version\nThis is perl, v5.10.1 (*) built for x86_64-linux-gnu-thread-multi\n"},{"id":"208770","messageId":"7vip66qu0u.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"8738xaqy40.fsf_-_@lifelogs.com","subject":"Re: [PATCHv5] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T22:24:01Z","receivedAt":"2013-02-05T22:24:01Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> +\tunless (scalar @entries) {\n> +\t\tif ($!) {\n> +\t\t\tlog_verbose(\"Unable to open $file: $!\");\n> +\t\t}\n> +\t\telse {\n\n\t} else {\n\n> +\t\t\tlog_verbose(\"No netrc entries found in $file\");\n> +\t\t}\n> +\n> +\tif ($gpgmode) {\n> +\t\t# typical shell character escapes from http://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html\n> +\t\tmy $f = $file;\n> +\t\t$f =~ s/([;<>\\*\\|`&\\$!#\\(\\)\\[\\]\\{\\}:'\"])/\\\\$1/g;\n\nYuck.  If you really have to quote, it is often far simpler to take\nadvantage of the fact that quoting rule for shell is much simpler\ninside '', i.e.\n\n\tsub sq {\n\t\tmy ($string) = @_;\n\t\t$string =~ s|'|'\\\\''|g;\n\t\treturn \"'$string'\";\n\t}\n\nThe only thing you have to worry about is a single quote, which\nwould close the single quote you want to be in, and you express it\nby first closing the single quote you opened, put the single quote\nby emitting a backslash and a single quote, and then immediately \nopen another single quote.\n\n> +\t\t# GPG doesn't work well with 2- or 3-argument open\n\nI think I commented on this in a separate message.\n\n> +\t# Net::Netrc does this, but the functionality is merged with the file\n> +\t# detection logic, so we have to extract just the part we need\n> +\tmy @netrc_entries = net_netrc_loader($io);\n> +\n> +\t# these entries will use the credential helper protocol token names\n> +\tmy @entries;\n> +\n> +\tforeach my $nentry (@netrc_entries) {\n> +\t\tmy %entry;\n> +\t\tmy $num_port;\n> +\n> +\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n> +\t\t\t$num_port = $nentry->{port};\n> +\t\t\tdelete $nentry->{port};\n> +\t\t}\n> +\n> +\t\t# create the new entry for the credential helper protocol\n> +\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n> +\n> +\t\t# for \"host X port Y\" where Y is an integer (captured by\n> +\t\t# $num_port above), set the host to \"X:Y\"\n> +\t\tif (defined $entry{host} && defined $num_port) {\n> +\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n> +\t\t}\n> +\n> +\t\tpush @entries, \\%entry;\n> +\t}\n> +\n> +\treturn @entries;\n> +}\n\nI'll leave this part to Peff to comment on.\n\n> +sub net_netrc_loader {\n> +\tmy $fh = shift @_;\n> +\tmy @entries;\n> +\tmy ($mach, $macdef, $tok, @tok);\n> +\n> +    LINE:\n> +\twhile (<$fh>) {\n> +\t\tundef $macdef if /\\A\\n\\Z/;\n> +\n> +\t\tif ($macdef) {\n> +\t\t\tnext LINE;\n> +\t\t}\n> +\n> +\t\ts/^\\s*//;\n> +\t\tchomp;\n> +\n> +\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n> +\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n> +\t\t\tpush(@tok, $tok);\n> +\t\t}\n> +\n> +\t    TOKEN:\n> +\t\twhile (@tok) {\n> +\t\t\tif ($tok[0] eq \"default\") {\n> +\t\t\t\tshift(@tok);\n> +\t\t\t\tundef $mach; # ignore 'default' lines\n\nI think it is saner to do something like this instead here:\n\n\t\t\t\t$mach = { machine => undef }\n\nOtherwise your log_debug() will be filled by the tokens used for the\ndefault entry, and also this \"undef $mach\" here will break your\nmacdef skipping logic if the default entry has a macdef, I think.\n\nYou can ignore an entry with undefined \"machine\" in the loop at the\nend of load_netrc.\n\n> +\t\t\t\tnext TOKEN;\n> +\t\t\t}\n> +\n> +\t\t\t$tok = shift(@tok);\n> +\n> +\t\t\tif ($tok eq \"machine\") {\n> +\t\t\t\tmy $host = shift @tok;\n> +\t\t\t\t$mach = { machine => $host };\n> +\t\t\t\tpush @entries, $mach;\n> +\t\t\t}\n> +\t\t\telsif (exists $options{tmap}->{$tok}) {\n> +\t\t\t\tunless ($mach) {\n> +\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n> +\t\t\t\t\tnext TOKEN;\n> +\t\t\t\t}\n> +\n> +\t\t\t\tmy $value = shift @tok;\n> +\t\t\t\tunless (defined $value) {\n> +\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n> +\t\t\t\t\tnext TOKEN;\n> +\t\t\t\t}\n> +\n> +\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n> +\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n> +\t\t\t\t$mach->{$tok} = $value;\n> +\t\t\t}\n> +\t\t\telsif ($tok eq \"macdef\") { # we ignore macros\n> +\t\t\t\tnext TOKEN unless $mach;\n> +\t\t\t\tmy $value = shift @tok;\n> +\t\t\t\t$macdef = 1;\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +\n> +\treturn @entries;\n> +}\n> +\n> +sub read_credential_data_from_stdin {\n> +\t# the query: start with every token with no value\n> +\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n> +\n> +\twhile (<STDIN>) {\n> +\t\tnext unless m/^([^=]+)=(.+)/;\n> +\n> +\t\tmy ($token, $value) = ($1, $2);\n> +\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n> +\t\t$q{$token} = $value;\n> +\t\tlog_debug(\"We were given search token $token and value $value\");\n> +\t}\n> +\n> +\tforeach (sort keys %q) {\n> +\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n> +\t}\n> +\n> +\treturn \\%q;\n> +}\n> +\n> +# takes the search tokens and then a list of entries\n> +# each entry is a hash reference\n> +sub find_netrc_entry {\n> +\tmy $query = shift @_;\n> +\n> +    ENTRY:\n> +\tforeach my $entry (@_)\n> +\t{\n> +\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n> +\t\tforeach my $check (sort keys %$query) {\n> +\t\t\tif (defined $query->{$check}) {\n> +\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n> +\t\t\t\t\t  $check,\n> +\t\t\t\t\t  $entry->{$check},\n> +\t\t\t\t\t  $query->{$check},\n> +\t\t\t\t\t  $entry_text);\n> +\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n> +\t\t\t\t\tnext ENTRY;\n> +\t\t\t\t}\n> +\t\t\t}\n> +\t\t\telse {\n> +\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\treturn $entry;\n> +\t}\n> +\n> +\t# nothing was found\n> +\treturn;\n> +}\n\nI'll leave this part to Peff to comment on.\n\n> +\n> +sub print_credential_data {\n> +\tmy $entry = shift @_;\n> +\tmy $query = shift @_;\n> +\n> +\tlog_debug(\"entry has passed all the search checks\");\n> + TOKEN:\n> +\tforeach my $git_token (sort keys %$entry) {\n> +\t\tlog_debug(\"looking for useful token $git_token\");\n> +\t\t# don't print unknown (to the credential helper protocol) tokens\n> +\t\tnext TOKEN unless exists $query->{$git_token};\n> +\n> +\t\t# don't print things asked in the query (the entry matches them)\n> +\t\tnext TOKEN if defined $query->{$git_token};\n> +\n> +\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n> +\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n> +\t}\n> +}\n> +sub log_verbose {\n> +\treturn unless $options{verbose};\n> +\tprintf STDERR @_;\n> +\tprintf STDERR \"\\n\";\n> +}\n> +\n> +sub log_debug {\n> +\treturn unless $options{debug};\n> +\tprintf STDERR @_;\n> +\tprintf STDERR \"\\n\";\n> +}\n\nOtherwise, looks almost ready to me.\n\nThanks.\n"},{"id":"208774","messageId":"87ehgupf63.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vpq0equo9.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv4] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-05T22:30:12Z","receivedAt":"2013-02-05T22:30:12Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 14:09:58 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> open $io, \"-|\", qw(gpg --decrypt), $ARGV[0]\n\nOK, the below will be in PATCHv6 (I'll wait on mailing it until after\nyou've reviewed the rest of PATCHv5).  Thanks for checking... I must\nhave had a typo or a missing comma or something, I could swear I tried\nexactly what you show.\n\nTed\n\n\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nindex 8298564..5f91630 100755\n--- a/contrib/credential/netrc/git-credential-netrc\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -230,13 +230,9 @@ sub load_netrc {\n \n \tmy $io;\n \tif ($gpgmode) {\n-\t\t# typical shell character escapes from http://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html\n-\t\tmy $f = $file;\n-\t\t$f =~ s/([;<>\\*\\|`&\\$!#\\(\\)\\[\\]\\{\\}:'\"])/\\\\$1/g;\n-\t\t# GPG doesn't work well with 2- or 3-argument open\n-\t\tmy $cmd = \"gpg --decrypt $f\";\n-\t\tlog_verbose(\"Using GPG to open $file: [$cmd]\");\n-\t\topen $io, \"$cmd|\";\n+\t\tmy @cmd = (qw(gpg --decrypt), $file);\n+\t\tlog_verbose(\"Using GPG to open $file: [@cmd]\");\n+\t\topen $io, \"-|\", @cmd;\n \t}\n \telse {\n \t\tlog_verbose(\"Opening $file...\");\n"},{"id":"208777","messageId":"7vtxpqnwiv.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"7vip66qu0u.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv5] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-05T23:58:16Z","receivedAt":"2013-02-05T23:58:16Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Otherwise, looks almost ready to me.\n\nFor now, I've queued this as a minimum fix-up on top of your patch\nand pushed the result out.  It is an equivalent of the previous\nreview comments in a patch form.  Please review and incorporate in\nyour reroll as appropriate.\n\nI haven't looked at the part that interacts with the credential\nsubsystem itself, though.\n\n contrib/credential/netrc/git-credential-netrc | 35 ++++++++++++---------------\n 1 file changed, 16 insertions(+), 19 deletions(-)\n\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nindex 8298564..30e05fb 100755\n--- a/contrib/credential/netrc/git-credential-netrc\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -74,6 +74,10 @@ Options:\n                        first matching entry found is returned via the credential\n                        helper protocol (see below).\n \n+                       When no -f option is given, .authinfo.gpg, .netrc.gpg,\n+\t\t       .authinfo, and .netrc files in your home directory are used\n+\t\t       in this order.\n+\n   -k|--insecure      : ignore bad file ownership or permissions\n \n   -d|--debug         : turn on debugging (developer info)\n@@ -206,8 +210,7 @@ foreach my $file (@$files) {\n \tunless (scalar @entries) {\n \t\tif ($!) {\n \t\t\tlog_verbose(\"Unable to open $file: $!\");\n-\t\t}\n-\t\telse {\n+\t\t} else {\n \t\t\tlog_verbose(\"No netrc entries found in $file\");\n \t\t}\n \n@@ -230,15 +233,10 @@ sub load_netrc {\n \n \tmy $io;\n \tif ($gpgmode) {\n-\t\t# typical shell character escapes from http://www.slac.stanford.edu/slac/www/resource/how-to-use/cgi-rexx/cgi-esc.html\n-\t\tmy $f = $file;\n-\t\t$f =~ s/([;<>\\*\\|`&\\$!#\\(\\)\\[\\]\\{\\}:'\"])/\\\\$1/g;\n-\t\t# GPG doesn't work well with 2- or 3-argument open\n-\t\tmy $cmd = \"gpg --decrypt $f\";\n-\t\tlog_verbose(\"Using GPG to open $file: [$cmd]\");\n-\t\topen $io, \"$cmd|\";\n-\t}\n-\telse {\n+\t\tmy @cmd = (qw(gpg --decrypt), $file)\n+\t\tlog_verbose(\"Using GPG to open $file: [@cmd]\");\n+\t\topen $io, \"-|\", @cmd;\n+\t} else {\n \t\tlog_verbose(\"Opening $file...\");\n \t\topen $io, '<', $file;\n \t}\n@@ -257,6 +255,9 @@ sub load_netrc {\n \t\tmy %entry;\n \t\tmy $num_port;\n \n+\t\tif (!defined $nentry->{machine}) {\n+\t\t\tnext;\n+\t\t}\n \t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n \t\t\t$num_port = $nentry->{port};\n \t\t\tdelete $nentry->{port};\n@@ -302,8 +303,7 @@ sub net_netrc_loader {\n \t\twhile (@tok) {\n \t\t\tif ($tok[0] eq \"default\") {\n \t\t\t\tshift(@tok);\n-\t\t\t\tundef $mach; # ignore 'default' lines\n-\n+\t\t\t\t$mach = { machine => undef }\n \t\t\t\tnext TOKEN;\n \t\t\t}\n \n@@ -313,8 +313,7 @@ sub net_netrc_loader {\n \t\t\t\tmy $host = shift @tok;\n \t\t\t\t$mach = { machine => $host };\n \t\t\t\tpush @entries, $mach;\n-\t\t\t}\n-\t\t\telsif (exists $options{tmap}->{$tok}) {\n+\t\t\t} elsif (exists $options{tmap}->{$tok}) {\n \t\t\t\tunless ($mach) {\n \t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n \t\t\t\t\tnext TOKEN;\n@@ -329,8 +328,7 @@ sub net_netrc_loader {\n \t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n \t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n \t\t\t\t$mach->{$tok} = $value;\n-\t\t\t}\n-\t\t\telsif ($tok eq \"macdef\") { # we ignore macros\n+\t\t\t} elsif ($tok eq \"macdef\") { # we ignore macros\n \t\t\t\tnext TOKEN unless $mach;\n \t\t\t\tmy $value = shift @tok;\n \t\t\t\t$macdef = 1;\n@@ -380,8 +378,7 @@ sub find_netrc_entry {\n \t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n \t\t\t\t\tnext ENTRY;\n \t\t\t\t}\n-\t\t\t}\n-\t\t\telse {\n+\t\t\t} else {\n \t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n \t\t\t}\n \t\t}\n-- \n1.8.1.2.641.g0b90ac4\n"},{"id":"208779","messageId":"87a9rip9fr.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vip66qu0u.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv5] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-06T00:34:00Z","receivedAt":"2013-02-06T00:34:00Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Tue, 05 Feb 2013 14:24:01 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\nJCH> Ted Zlatanov <tzz@lifelogs.com> writes:\n>> +\t\t$f =~ s/([;<>\\*\\|`&\\$!#\\(\\)\\[\\]\\{\\}:'\"])/\\\\$1/g;\n\nJCH> Yuck.  If you really have to quote, it is often far simpler to take\nJCH> advantage of the fact that quoting rule for shell is much simpler\nJCH> inside '', i.e.\n\nJCH> \tsub sq {\nJCH> \t\tmy ($string) = @_;\nJCH> \t\t$string =~ s|'|'\\\\''|g;\nJCH> \t\treturn \"'$string'\";\nJCH> \t}\n\nOh, that's nice.  Thanks.  We don't need it anymore, but I'm sad to see\nit go unused.\n\nJCH> I think it is saner to do something like this instead here:\nJCH> \t\t\t\t$mach = { machine => undef }\n\nJCH> Otherwise your log_debug() will be filled by the tokens used for the\nJCH> default entry, and also this \"undef $mach\" here will break your\nJCH> macdef skipping logic if the default entry has a macdef, I think.\n\nJCH> You can ignore an entry with undefined \"machine\" in the loop at the\nJCH> end of load_netrc.\n\nCool, I merged your changes into PATCHv6.  I'll keep in mind about\nmerging the trailing else braces, too.  I forgot that setting for\ncperl-mode (`cperl-merge-trailing-else . t').\n\nThanks\nTed\n"},{"id":"208781","messageId":"876226p97h.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"7vtxpqnwiv.fsf@alter.siamese.dyndns.org","subject":"[PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-06T00:38:58Z","receivedAt":"2013-02-06T00:38:58Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"Add Git credential helper that can parse netrc/authinfo files.\n\nThis credential helper supports multiple files, returning the first one\nthat matches.  It checks file permissions and owner.  For *.gpg files,\nit will run GPG to decrypt the file.\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\nChanges since PATCHv5:\n\n- reroll from tz/credential-authinfo:\n * brace fixes\n * list attempted default files in help doc\n * 3-arg open() for the GPG pipe\n * instead of skipping 'default' tokens, store them as machine => undef\n\n contrib/credential/netrc/Makefile             |   12 +\n contrib/credential/netrc/git-credential-netrc |  421 +++++++++++++++++++++++++\n 2 files changed, 433 insertions(+), 0 deletions(-)\n create mode 100644 contrib/credential/netrc/Makefile\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n\ndiff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\nnew file mode 100644\nindex 0000000..18a924f\n--- /dev/null\n+++ b/contrib/credential/netrc/Makefile\n@@ -0,0 +1,12 @@\n+test_netrc:\n+\t@(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring failure\"\n+\t@echo \"=> Silent invocation... nothing should show up here with a missing file\"\n+\t@echo \"bad data\" | ./git-credential-netrc -f A get\n+\t@echo \"=> Back to noisy: -v and -d used below, missing file\"\n+\techo \"bad data\" | ./git-credential-netrc -f A -d -v get\n+\t@echo \"=> Look for any entry in the default file set\"\n+\techo \"\" | ./git-credential-netrc -d -v get\n+\t@echo \"=> Look for github.com in the default file set\"\n+\techo \"host=google.com\" | ./git-credential-netrc -d -v get\n+\t@echo \"=> Look for a nonexistent machine in the default file set\"\n+\techo \"host=korovamilkbar\" | ./git-credential-netrc -d -v get\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..30e05fb\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,421 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+\t       help => 0,\n+\t       debug => 0,\n+\t       verbose => 0,\n+\t       insecure => 0,\n+\t       file => [],\n+\n+\t       # identical token maps, e.g. host -> host, will be inserted later\n+\t       tmap => {\n+\t\t\tport => 'protocol',\n+\t\t\tmachine => 'host',\n+\t\t\tpath => 'path',\n+\t\t\tlogin => 'username',\n+\t\t\tuser => 'username',\n+\t\t\tpassword => 'password',\n+\t\t       }\n+\t      );\n+\n+# Map each credential protocol token to itself on the netrc side.\n+foreach (values %{$options{tmap}}) {\n+\t$options{tmap}->{$_} = $_;\n+}\n+\n+# Now, $options{tmap} has a mapping from the netrc format to the Git credential\n+# helper protocol.\n+\n+# Next, we build the reverse token map.\n+\n+# When $rmap{foo} contains 'bar', that means that what the Git credential helper\n+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in\n+# %rmap are what we expect to read from the netrc/authinfo file.\n+\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"insecure|k\",\n+           \"verbose|v\",\n+           \"file|f=s@\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+\n+  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n+                       will be decrypted by GPG before parsing.  Multiple -f\n+                       arguments are OK.  They are processed in order, and the\n+                       first matching entry found is returned via the credential\n+                       helper protocol (see below).\n+\n+                       When no -f option is given, .authinfo.gpg, .netrc.gpg,\n+\t\t       .authinfo, and .netrc files in your home directory are used\n+\t\t       in this order.\n+\n+  -k|--insecure      : ignore bad file ownership or permissions\n+\n+  -d|--debug         : turn on debugging (developer info)\n+\n+  -v|--verbose       : be more verbose (show files and information found)\n+\n+To enable this credential helper:\n+\n+  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'\n+\n+(Note that Git will prepend \"git-credential-\" to the helper name and look for it\n+in the path.)\n+\n+...and if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+...or to see the files opened and data found:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -v'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens every AUTHFILE\n+and looks for the first entry that matches the requested search criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+host=github.com\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for the first entry in every AUTHFILE that\n+matches\n+\n+machine github.com port https login tzz\n+\n+OR\n+\n+machine github.com protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the entry, including\n+\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n+back to \"protocol\".  Any redundant entry tokens (part of the original query) are\n+skipped.\n+\n+Again, note that only the first matching entry from all the AUTHFILEs, processed\n+in the sequence given on the command line, is used.\n+\n+Netrc/authinfo tokens can be quoted as 'STRING' or \"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit 0;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# Credentials must get a parameter, so die if it's missing.\n+die \"Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get\" unless defined $mode;\n+\n+# Only support 'get' mode; with any other unsupported ones we just exit.\n+exit 0 unless $mode eq 'get';\n+\n+my $files = $options{file};\n+\n+# if no files were given, use a predefined list.\n+# note that .gpg files come first\n+unless (scalar @$files) {\n+\tmy @candidates = qw[\n+\t\t\t\t   ~/.authinfo.gpg\n+\t\t\t\t   ~/.netrc.gpg\n+\t\t\t\t   ~/.authinfo\n+\t\t\t\t   ~/.netrc\n+\t\t\t  ];\n+\n+\t$files = $options{file} = [ map { glob $_ } @candidates ];\n+}\n+\n+my $query = read_credential_data_from_stdin();\n+\n+FILE:\n+foreach my $file (@$files) {\n+\tmy $gpgmode = $file =~ m/\\.gpg$/;\n+\tunless (-r $file) {\n+\t\tlog_verbose(\"Unable to read $file; skipping it\");\n+\t\tnext FILE;\n+\t}\n+\n+\t# the following check is copied from Net::Netrc, for non-GPG files\n+\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n+\tunless ($gpgmode || $options{insecure} ||\n+\t\t$^O eq 'os2'\n+\t\t|| $^O eq 'MSWin32'\n+\t\t|| $^O eq 'MacOS'\n+\t\t|| $^O =~ /^cygwin/) {\n+\t\tmy @stat = stat($file);\n+\n+\t\tif (@stat) {\n+\t\t\tif ($stat[2] & 077) {\n+\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n+\t\t\t\t\t    $stat[2] & 07777);\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\n+\t\t\tif ($stat[4] != $<) {\n+\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tmy @entries = load_netrc($file, $gpgmode);\n+\n+\tunless (scalar @entries) {\n+\t\tif ($!) {\n+\t\t\tlog_verbose(\"Unable to open $file: $!\");\n+\t\t} else {\n+\t\t\tlog_verbose(\"No netrc entries found in $file\");\n+\t\t}\n+\n+\t\tnext FILE;\n+\t}\n+\n+\tmy $entry = find_netrc_entry($query, @entries);\n+\tif ($entry) {\n+\t\tprint_credential_data($entry, $query);\n+\t\t# we're done!\n+\t\tlast FILE;\n+\t}\n+}\n+\n+exit 0;\n+\n+sub load_netrc {\n+\tmy $file = shift @_;\n+\tmy $gpgmode = shift @_;\n+\n+\tmy $io;\n+\tif ($gpgmode) {\n+\t\tmy @cmd = (qw(gpg --decrypt), $file)\n+\t\tlog_verbose(\"Using GPG to open $file: [@cmd]\");\n+\t\topen $io, \"-|\", @cmd;\n+\t} else {\n+\t\tlog_verbose(\"Opening $file...\");\n+\t\topen $io, '<', $file;\n+\t}\n+\n+\t# nothing to do if the open failed (we log the error later)\n+\treturn unless $io;\n+\n+\t# Net::Netrc does this, but the functionality is merged with the file\n+\t# detection logic, so we have to extract just the part we need\n+\tmy @netrc_entries = net_netrc_loader($io);\n+\n+\t# these entries will use the credential helper protocol token names\n+\tmy @entries;\n+\n+\tforeach my $nentry (@netrc_entries) {\n+\t\tmy %entry;\n+\t\tmy $num_port;\n+\n+\t\tif (!defined $nentry->{machine}) {\n+\t\t\tnext;\n+\t\t}\n+\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n+\t\t\t$num_port = $nentry->{port};\n+\t\t\tdelete $nentry->{port};\n+\t\t}\n+\n+\t\t# create the new entry for the credential helper protocol\n+\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n+\n+\t\t# for \"host X port Y\" where Y is an integer (captured by\n+\t\t# $num_port above), set the host to \"X:Y\"\n+\t\tif (defined $entry{host} && defined $num_port) {\n+\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n+\t\t}\n+\n+\t\tpush @entries, \\%entry;\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub net_netrc_loader {\n+\tmy $fh = shift @_;\n+\tmy @entries;\n+\tmy ($mach, $macdef, $tok, @tok);\n+\n+    LINE:\n+\twhile (<$fh>) {\n+\t\tundef $macdef if /\\A\\n\\Z/;\n+\n+\t\tif ($macdef) {\n+\t\t\tnext LINE;\n+\t\t}\n+\n+\t\ts/^\\s*//;\n+\t\tchomp;\n+\n+\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\t\tpush(@tok, $tok);\n+\t\t}\n+\n+\t    TOKEN:\n+\t\twhile (@tok) {\n+\t\t\tif ($tok[0] eq \"default\") {\n+\t\t\t\tshift(@tok);\n+\t\t\t\t$mach = { machine => undef }\n+\t\t\t\tnext TOKEN;\n+\t\t\t}\n+\n+\t\t\t$tok = shift(@tok);\n+\n+\t\t\tif ($tok eq \"machine\") {\n+\t\t\t\tmy $host = shift @tok;\n+\t\t\t\t$mach = { machine => $host };\n+\t\t\t\tpush @entries, $mach;\n+\t\t\t} elsif (exists $options{tmap}->{$tok}) {\n+\t\t\t\tunless ($mach) {\n+\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\tunless (defined $value) {\n+\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n+\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n+\t\t\t\t$mach->{$tok} = $value;\n+\t\t\t} elsif ($tok eq \"macdef\") { # we ignore macros\n+\t\t\t\tnext TOKEN unless $mach;\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\t$macdef = 1;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub read_credential_data_from_stdin {\n+\t# the query: start with every token with no value\n+\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+\twhile (<STDIN>) {\n+\t\tnext unless m/^([^=]+)=(.+)/;\n+\n+\t\tmy ($token, $value) = ($1, $2);\n+\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n+\t\t$q{$token} = $value;\n+\t\tlog_debug(\"We were given search token $token and value $value\");\n+\t}\n+\n+\tforeach (sort keys %q) {\n+\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n+\t}\n+\n+\treturn \\%q;\n+}\n+\n+# takes the search tokens and then a list of entries\n+# each entry is a hash reference\n+sub find_netrc_entry {\n+\tmy $query = shift @_;\n+\n+    ENTRY:\n+\tforeach my $entry (@_)\n+\t{\n+\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n+\t\tforeach my $check (sort keys %$query) {\n+\t\t\tif (defined $query->{$check}) {\n+\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n+\t\t\t\t\t  $check,\n+\t\t\t\t\t  $entry->{$check},\n+\t\t\t\t\t  $query->{$check},\n+\t\t\t\t\t  $entry_text);\n+\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n+\t\t\t\t\tnext ENTRY;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn $entry;\n+\t}\n+\n+\t# nothing was found\n+\treturn;\n+}\n+\n+sub print_credential_data {\n+\tmy $entry = shift @_;\n+\tmy $query = shift @_;\n+\n+\tlog_debug(\"entry has passed all the search checks\");\n+ TOKEN:\n+\tforeach my $git_token (sort keys %$entry) {\n+\t\tlog_debug(\"looking for useful token $git_token\");\n+\t\t# don't print unknown (to the credential helper protocol) tokens\n+\t\tnext TOKEN unless exists $query->{$git_token};\n+\n+\t\t# don't print things asked in the query (the entry matches them)\n+\t\tnext TOKEN if defined $query->{$git_token};\n+\n+\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n+\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n+\t}\n+}\n+sub log_verbose {\n+\treturn unless $options{verbose};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n+\n+sub log_debug {\n+\treturn unless $options{debug};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n-- \n1.7.9.rc2\n"},{"id":"208958","messageId":"7vtxpn4r7a.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"876226p97h.fsf_-_@lifelogs.com","subject":"Re: [PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-07T23:52:41Z","receivedAt":"2013-02-07T23:52:41Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> Add Git credential helper that can parse netrc/authinfo files.\n\nI think this line is redundant; we already know it on the Subject: line.\n\n> This credential helper supports multiple files, returning the first one\n> that matches.  It checks file permissions and owner.  For *.gpg files,\n> it will run GPG to decrypt the file.\n>\n> Signed-off-by: Ted Zlatanov <tzz@lifelogs.com>\n> ---\n> ...\n> diff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\n> new file mode 100644\n> index 0000000..18a924f\n> --- /dev/null\n> +++ b/contrib/credential/netrc/Makefile\n> @@ -0,0 +1,12 @@\n> +test_netrc:\n> +\t@(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring failure\"\n> +\t@echo \"=> Silent invocation... nothing should show up here with a missing file\"\n> +\t@echo \"bad data\" | ./git-credential-netrc -f A get\n> +\t@echo \"=> Back to noisy: -v and -d used below, missing file\"\n> +\techo \"bad data\" | ./git-credential-netrc -f A -d -v get\n> +\t@echo \"=> Look for any entry in the default file set\"\n> +\techo \"\" | ./git-credential-netrc -d -v get\n> +\t@echo \"=> Look for github.com in the default file set\"\n> +\techo \"host=google.com\" | ./git-credential-netrc -d -v get\n> +\t@echo \"=> Look for a nonexistent machine in the default file set\"\n> +\techo \"host=korovamilkbar\" | ./git-credential-netrc -d -v get\n\nWhose netrc is this reading?\n\nDon't we want all of them to have \"-f A\" and ship \"A\" (rename it to\nsomething more reasonable), so that anybody can notice when he tries\nto improve it and breaks it?\n"},{"id":"208963","messageId":"87mwvfbmgi.fsf@lifelogs.com","threadId":"32825","inReplyTo":"7vtxpn4r7a.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-08T01:53:17Z","receivedAt":"2013-02-08T01:53:17Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Thu, 07 Feb 2013 15:52:41 -0800 Junio C Hamano <gitster@pobox.com> wrote: \n\n>> +\t@echo \"=> Look for any entry in the default file set\"\n>> +\techo \"\" | ./git-credential-netrc -d -v get\n>> +\t@echo \"=> Look for github.com in the default file set\"\n>> +\techo \"host=google.com\" | ./git-credential-netrc -d -v get\n>> +\t@echo \"=> Look for a nonexistent machine in the default file set\"\n>> +\techo \"host=korovamilkbar\" | ./git-credential-netrc -d -v get\n\nJCH> Whose netrc is this reading?\n\nJCH> Don't we want all of them to have \"-f A\" and ship \"A\" (rename it to\nJCH> something more reasonable), so that anybody can notice when he tries\nJCH> to improve it and breaks it?\n\nI agree this Makefile is not a good test to ship out.  It was my quickie\ntest rig that I should have reworked before adding to the patch.  Sorry.\n\nI see contrib/subtree/t and contrib/mw-to-git/t that I could copy.  The\ntest will have a few files to parse, and will be able to compare the\nexpected to the actual output.  Does that sound like a good plan?\n\nTed\n"},{"id":"208978","messageId":"7vhaln49gr.fsf@alter.siamese.dyndns.org","threadId":"32825","inReplyTo":"87mwvfbmgi.fsf@lifelogs.com","subject":"Re: [PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-08T06:15:48Z","receivedAt":"2013-02-08T06:15:48Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ted Zlatanov <tzz@lifelogs.com> writes:\n\n> I agree this Makefile is not a good test to ship out.  It was my quickie\n> test rig that I should have reworked before adding to the patch.  Sorry.\n\nNothing to be sorry about.  Starting with quick-and-dirty and\npolishing for public consumption is what the review cycle is about,\nand we are here to help that process.\n\n> I see contrib/subtree/t and contrib/mw-to-git/t that I could copy.  The\n> test will have a few files to parse, and will be able to compare the\n> expected to the actual output.  Does that sound like a good plan?\n\nYup.\n\nThanks.\n"},{"id":"208979","messageId":"20130208061855.GA11892@sigill.intra.peff.net","threadId":"32825","inReplyTo":"876226p97h.fsf_-_@lifelogs.com","subject":"Re: [PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2013-02-08T06:18:55Z","receivedAt":"2013-02-08T06:18:55Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Feb 05, 2013 at 07:38:58PM -0500, Ted Zlatanov wrote:\n\n> Add Git credential helper that can parse netrc/authinfo files.\n> \n> This credential helper supports multiple files, returning the first one\n> that matches.  It checks file permissions and owner.  For *.gpg files,\n> it will run GPG to decrypt the file.\n> \n> Signed-off-by: Ted Zlatanov <tzz@lifelogs.com>\n\n\n> +\t# the following check is copied from Net::Netrc, for non-GPG files\n> +\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n\ns/compatable/compatible/\n\nYou mention os/2 and Win32 here, but the check has more:\n\n> +\tunless ($gpgmode || $options{insecure} ||\n> +\t\t$^O eq 'os2'\n> +\t\t|| $^O eq 'MSWin32'\n> +\t\t|| $^O eq 'MacOS'\n> +\t\t|| $^O =~ /^cygwin/) {\n\nDoes MacOS really not handle stat? Or is this old MacOS, not OS X?\n\n> +sub load_netrc {\n> [...]\n> +\tforeach my $nentry (@netrc_entries) {\n> +\t\tmy %entry;\n> +\t\tmy $num_port;\n> +\n> +\t\tif (!defined $nentry->{machine}) {\n> +\t\t\tnext;\n> +\t\t}\n> +\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n> +\t\t\t$num_port = $nentry->{port};\n> +\t\t\tdelete $nentry->{port};\n> +\t\t}\n> +\n> +\t\t# create the new entry for the credential helper protocol\n> +\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n> +\n> +\t\t# for \"host X port Y\" where Y is an integer (captured by\n> +\t\t# $num_port above), set the host to \"X:Y\"\n> +\t\tif (defined $entry{host} && defined $num_port) {\n> +\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n> +\t\t}\n\nSo this will convert:\n\n  machine foo port smtp\n\nin the netrc into (protocol => \"smtp\", host => \"foo\"), but:\n\n  machine foo port 25\n\ninto (protocol => undef, host => \"foo:25\"), right? That makes sense to\nme.\n\n> +sub net_netrc_loader {\n> [...]\n\nI won't comment here, as I know very little about netrc (I always\nthought it was line-oriented, too!) and Junio has covered it.\n\n> +# takes the search tokens and then a list of entries\n> +# each entry is a hash reference\n> +sub find_netrc_entry {\n> +\tmy $query = shift @_;\n> +\n> +    ENTRY:\n> +\tforeach my $entry (@_)\n> +\t{\n> +\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n> +\t\tforeach my $check (sort keys %$query) {\n> +\t\t\tif (defined $query->{$check}) {\n> +\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n> +\t\t\t\t\t  $check,\n> +\t\t\t\t\t  $entry->{$check},\n> +\t\t\t\t\t  $query->{$check},\n> +\t\t\t\t\t  $entry_text);\n> +\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n> +\t\t\t\t\tnext ENTRY;\n> +\t\t\t\t}\n> +\t\t\t} else {\n> +\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n> +\t\t\t}\n\nThis looks right to me.\n\n> +sub print_credential_data {\n\nI don't know if you want to take the hit of relying on Git.pm (it is\nnice for the helper to be totally standalone and copy-able), but one\nobvious possible refactor would be to use the credential read/write\nfunctions recently added there. I'm OK with not doing that, though.\n\n> +\tmy $entry = shift @_;\n> +\tmy $query = shift @_;\n> +\n> +\tlog_debug(\"entry has passed all the search checks\");\n> + TOKEN:\n> +\tforeach my $git_token (sort keys %$entry) {\n> +\t\tlog_debug(\"looking for useful token $git_token\");\n> +\t\t# don't print unknown (to the credential helper protocol) tokens\n> +\t\tnext TOKEN unless exists $query->{$git_token};\n> +\n> +\t\t# don't print things asked in the query (the entry matches them)\n> +\t\tnext TOKEN if defined $query->{$git_token};\n> +\n> +\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n> +\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n> +\t}\n\nPrintf? Bleh, isn't this supposed to be perl? :P\n\nI don't see anything wrong from the credential-handling side of things.\nAs I said, I didn't look closely at the netrc parsing bits. From my\nreading of \"perldoc macos\", the answer to my question above is \"yes,\nstat doesn't work on MacOS Classic\". So I think the script itself is\nfine.\n\nIn your tests:\n\n> +++ b/contrib/credential/netrc/Makefile\n> @@ -0,0 +1,12 @@\n> +test_netrc:\n> +       @(echo \"bad data\" | ./git-credential-netrc -f A -d -v) || echo \"Bad invocation test, ignoring\n> failure\"\n> +       @echo \"=> Silent invocation... nothing should show up here with a missing file\"\n> +       @echo \"bad data\" | ./git-credential-netrc -f A get\n> +       @echo \"=> Back to noisy: -v and -d used below, missing file\"\n> +       echo \"bad data\" | ./git-credential-netrc -f A -d -v get\n> +       @echo \"=> Look for any entry in the default file set\"\n> +       echo \"\" | ./git-credential-netrc -d -v get\n> +       @echo \"=> Look for github.com in the default file set\"\n> +       echo \"host=google.com\" | ./git-credential-netrc -d -v get\n> +       @echo \"=> Look for a nonexistent machine in the default file set\"\n> +       echo \"host=korovamilkbar\" | ./git-credential-netrc -d -v get\n\nYou are depending on whatever the user has in their ~/.netrc, no?\nWouldn't it make more sense to ship a sample netrc and run all of the\ntests with \"-f netrc.example\"?\n\nIt may also be worth building on top of the regular git test harness.\nIt's more work, but the resulting code (and the output) will be much\nmore readable.\n\n-Peff\n"},{"id":"210250","messageId":"877glwv1fo.fsf_-_@lifelogs.com","threadId":"32825","inReplyTo":"876226p97h.fsf_-_@lifelogs.com","subject":"[PATCH v7] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-25T15:49:15Z","receivedAt":"2013-02-25T15:49:15Z","isPatch":true,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"This credential helper supports multiple files, returning the first one\nthat matches.  It checks file permissions and owner.  For *.gpg files,\nit will run GPG to decrypt the file.\n\nSigned-off-by: Ted Zlatanov <tzz@lifelogs.com>\n---\nChanges since PATCHv6:\n\n- change Makefile test to test.pl (using Perl Test module) + test.netrc\n * `make test' runs all the tests in the standard Test format\n * `make testverbose' runs the tests with -d -v to see what's happening\n- fix missing semicolons and minor typos\n\n contrib/credential/netrc/Makefile             |    5 +\n contrib/credential/netrc/git-credential-netrc |  421 +++++++++++++++++++++++++\n contrib/credential/netrc/test.netrc           |   13 +\n contrib/credential/netrc/test.pl              |  106 +++++++\n 4 files changed, 545 insertions(+), 0 deletions(-)\n create mode 100644 contrib/credential/netrc/Makefile\n create mode 100755 contrib/credential/netrc/git-credential-netrc\n create mode 100644 contrib/credential/netrc/test.netrc\n create mode 100755 contrib/credential/netrc/test.pl\n\ndiff --git a/contrib/credential/netrc/Makefile b/contrib/credential/netrc/Makefile\nnew file mode 100644\nindex 0000000..51b7613\n--- /dev/null\n+++ b/contrib/credential/netrc/Makefile\n@@ -0,0 +1,5 @@\n+test:\n+\t./test.pl\n+\n+testverbose:\n+\t./test.pl -d -v\ndiff --git a/contrib/credential/netrc/git-credential-netrc b/contrib/credential/netrc/git-credential-netrc\nnew file mode 100755\nindex 0000000..6c51c43\n--- /dev/null\n+++ b/contrib/credential/netrc/git-credential-netrc\n@@ -0,0 +1,421 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use warnings;\n+\n+use Getopt::Long;\n+use File::Basename;\n+\n+my $VERSION = \"0.1\";\n+\n+my %options = (\n+\t       help => 0,\n+\t       debug => 0,\n+\t       verbose => 0,\n+\t       insecure => 0,\n+\t       file => [],\n+\n+\t       # identical token maps, e.g. host -> host, will be inserted later\n+\t       tmap => {\n+\t\t\tport => 'protocol',\n+\t\t\tmachine => 'host',\n+\t\t\tpath => 'path',\n+\t\t\tlogin => 'username',\n+\t\t\tuser => 'username',\n+\t\t\tpassword => 'password',\n+\t\t       }\n+\t      );\n+\n+# Map each credential protocol token to itself on the netrc side.\n+foreach (values %{$options{tmap}}) {\n+\t$options{tmap}->{$_} = $_;\n+}\n+\n+# Now, $options{tmap} has a mapping from the netrc format to the Git credential\n+# helper protocol.\n+\n+# Next, we build the reverse token map.\n+\n+# When $rmap{foo} contains 'bar', that means that what the Git credential helper\n+# protocol calls 'bar' is found as 'foo' in the netrc/authinfo file.  Keys in\n+# %rmap are what we expect to read from the netrc/authinfo file.\n+\n+my %rmap;\n+foreach my $k (keys %{$options{tmap}}) {\n+\tpush @{$rmap{$options{tmap}->{$k}}}, $k;\n+}\n+\n+Getopt::Long::Configure(\"bundling\");\n+\n+# TODO: maybe allow the token map $options{tmap} to be configurable.\n+GetOptions(\\%options,\n+           \"help|h\",\n+           \"debug|d\",\n+           \"insecure|k\",\n+           \"verbose|v\",\n+           \"file|f=s@\",\n+          );\n+\n+if ($options{help}) {\n+\tmy $shortname = basename($0);\n+\t$shortname =~ s/git-credential-//;\n+\n+\tprint <<EOHIPPUS;\n+\n+$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get\n+\n+Version $VERSION by tzz\\@lifelogs.com.  License: BSD.\n+\n+Options:\n+\n+  -f|--file AUTHFILE : specify netrc-style files.  Files with the .gpg extension\n+                       will be decrypted by GPG before parsing.  Multiple -f\n+                       arguments are OK.  They are processed in order, and the\n+                       first matching entry found is returned via the credential\n+                       helper protocol (see below).\n+\n+                       When no -f option is given, .authinfo.gpg, .netrc.gpg,\n+\t\t       .authinfo, and .netrc files in your home directory are used\n+\t\t       in this order.\n+\n+  -k|--insecure      : ignore bad file ownership or permissions\n+\n+  -d|--debug         : turn on debugging (developer info)\n+\n+  -v|--verbose       : be more verbose (show files and information found)\n+\n+To enable this credential helper:\n+\n+  git config credential.helper '$shortname -f AUTHFILE1 -f AUTHFILE2'\n+\n+(Note that Git will prepend \"git-credential-\" to the helper name and look for it\n+in the path.)\n+\n+...and if you want lots of debugging info:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -d'\n+\n+...or to see the files opened and data found:\n+\n+  git config credential.helper '$shortname -f AUTHFILE -v'\n+\n+Only \"get\" mode is supported by this credential helper.  It opens every AUTHFILE\n+and looks for the first entry that matches the requested search criteria:\n+\n+ 'port|protocol':\n+   The protocol that will be used (e.g., https). (protocol=X)\n+\n+ 'machine|host':\n+   The remote hostname for a network credential. (host=X)\n+\n+ 'path':\n+   The path with which the credential will be used. (path=X)\n+\n+ 'login|user|username':\n+   The credential’s username, if we already have one. (username=X)\n+\n+Thus, when we get this query on STDIN:\n+\n+host=github.com\n+protocol=https\n+username=tzz\n+\n+this credential helper will look for the first entry in every AUTHFILE that\n+matches\n+\n+machine github.com port https login tzz\n+\n+OR\n+\n+machine github.com protocol https login tzz\n+\n+OR... etc. acceptable tokens as listed above.  Any unknown tokens are\n+simply ignored.\n+\n+Then, the helper will print out whatever tokens it got from the entry, including\n+\"password\" tokens, mapping back to Git's helper protocol; e.g. \"port\" is mapped\n+back to \"protocol\".  Any redundant entry tokens (part of the original query) are\n+skipped.\n+\n+Again, note that only the first matching entry from all the AUTHFILEs, processed\n+in the sequence given on the command line, is used.\n+\n+Netrc/authinfo tokens can be quoted as 'STRING' or \"STRING\".\n+\n+No caching is performed by this credential helper.\n+\n+EOHIPPUS\n+\n+\texit 0;\n+}\n+\n+my $mode = shift @ARGV;\n+\n+# Credentials must get a parameter, so die if it's missing.\n+die \"Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get\" unless defined $mode;\n+\n+# Only support 'get' mode; with any other unsupported ones we just exit.\n+exit 0 unless $mode eq 'get';\n+\n+my $files = $options{file};\n+\n+# if no files were given, use a predefined list.\n+# note that .gpg files come first\n+unless (scalar @$files) {\n+\tmy @candidates = qw[\n+\t\t\t\t   ~/.authinfo.gpg\n+\t\t\t\t   ~/.netrc.gpg\n+\t\t\t\t   ~/.authinfo\n+\t\t\t\t   ~/.netrc\n+\t\t\t  ];\n+\n+\t$files = $options{file} = [ map { glob $_ } @candidates ];\n+}\n+\n+my $query = read_credential_data_from_stdin();\n+\n+FILE:\n+foreach my $file (@$files) {\n+\tmy $gpgmode = $file =~ m/\\.gpg$/;\n+\tunless (-r $file) {\n+\t\tlog_verbose(\"Unable to read $file; skipping it\");\n+\t\tnext FILE;\n+\t}\n+\n+\t# the following check is copied from Net::Netrc, for non-GPG files\n+\t# OS/2 and Win32 do not handle stat in a way compatible with this check :-(\n+\tunless ($gpgmode || $options{insecure} ||\n+\t\t$^O eq 'os2'\n+\t\t|| $^O eq 'MSWin32'\n+\t\t|| $^O eq 'MacOS'\n+\t\t|| $^O =~ /^cygwin/) {\n+\t\tmy @stat = stat($file);\n+\n+\t\tif (@stat) {\n+\t\t\tif ($stat[2] & 077) {\n+\t\t\t\tlog_verbose(\"Insecure $file (mode=%04o); skipping it\",\n+\t\t\t\t\t    $stat[2] & 07777);\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\n+\t\t\tif ($stat[4] != $<) {\n+\t\t\t\tlog_verbose(\"Not owner of $file; skipping it\");\n+\t\t\t\tnext FILE;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tmy @entries = load_netrc($file, $gpgmode);\n+\n+\tunless (scalar @entries) {\n+\t\tif ($!) {\n+\t\t\tlog_verbose(\"Unable to open $file: $!\");\n+\t\t} else {\n+\t\t\tlog_verbose(\"No netrc entries found in $file\");\n+\t\t}\n+\n+\t\tnext FILE;\n+\t}\n+\n+\tmy $entry = find_netrc_entry($query, @entries);\n+\tif ($entry) {\n+\t\tprint_credential_data($entry, $query);\n+\t\t# we're done!\n+\t\tlast FILE;\n+\t}\n+}\n+\n+exit 0;\n+\n+sub load_netrc {\n+\tmy $file = shift @_;\n+\tmy $gpgmode = shift @_;\n+\n+\tmy $io;\n+\tif ($gpgmode) {\n+\t\tmy @cmd = (qw(gpg --decrypt), $file);\n+\t\tlog_verbose(\"Using GPG to open $file: [@cmd]\");\n+\t\topen $io, \"-|\", @cmd;\n+\t} else {\n+\t\tlog_verbose(\"Opening $file...\");\n+\t\topen $io, '<', $file;\n+\t}\n+\n+\t# nothing to do if the open failed (we log the error later)\n+\treturn unless $io;\n+\n+\t# Net::Netrc does this, but the functionality is merged with the file\n+\t# detection logic, so we have to extract just the part we need\n+\tmy @netrc_entries = net_netrc_loader($io);\n+\n+\t# these entries will use the credential helper protocol token names\n+\tmy @entries;\n+\n+\tforeach my $nentry (@netrc_entries) {\n+\t\tmy %entry;\n+\t\tmy $num_port;\n+\n+\t\tif (!defined $nentry->{machine}) {\n+\t\t\tnext;\n+\t\t}\n+\t\tif (defined $nentry->{port} && $nentry->{port} =~ m/^\\d+$/) {\n+\t\t\t$num_port = $nentry->{port};\n+\t\t\tdelete $nentry->{port};\n+\t\t}\n+\n+\t\t# create the new entry for the credential helper protocol\n+\t\t$entry{$options{tmap}->{$_}} = $nentry->{$_} foreach keys %$nentry;\n+\n+\t\t# for \"host X port Y\" where Y is an integer (captured by\n+\t\t# $num_port above), set the host to \"X:Y\"\n+\t\tif (defined $entry{host} && defined $num_port) {\n+\t\t\t$entry{host} = join(':', $entry{host}, $num_port);\n+\t\t}\n+\n+\t\tpush @entries, \\%entry;\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub net_netrc_loader {\n+\tmy $fh = shift @_;\n+\tmy @entries;\n+\tmy ($mach, $macdef, $tok, @tok);\n+\n+    LINE:\n+\twhile (<$fh>) {\n+\t\tundef $macdef if /\\A\\n\\Z/;\n+\n+\t\tif ($macdef) {\n+\t\t\tnext LINE;\n+\t\t}\n+\n+\t\ts/^\\s*//;\n+\t\tchomp;\n+\n+\t\twhile (length && s/^(\"((?:[^\"]+|\\\\.)*)\"|((?:[^\\\\\\s]+|\\\\.)*))\\s*//) {\n+\t\t\t(my $tok = $+) =~ s/\\\\(.)/$1/g;\n+\t\t\tpush(@tok, $tok);\n+\t\t}\n+\n+\t    TOKEN:\n+\t\twhile (@tok) {\n+\t\t\tif ($tok[0] eq \"default\") {\n+\t\t\t\tshift(@tok);\n+\t\t\t\t$mach = { machine => undef };\n+\t\t\t\tnext TOKEN;\n+\t\t\t}\n+\n+\t\t\t$tok = shift(@tok);\n+\n+\t\t\tif ($tok eq \"machine\") {\n+\t\t\t\tmy $host = shift @tok;\n+\t\t\t\t$mach = { machine => $host };\n+\t\t\t\tpush @entries, $mach;\n+\t\t\t} elsif (exists $options{tmap}->{$tok}) {\n+\t\t\t\tunless ($mach) {\n+\t\t\t\t\tlog_debug(\"Skipping token $tok because no machine was given\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\tunless (defined $value) {\n+\t\t\t\t\tlog_debug(\"Token $tok had no value, skipping it.\");\n+\t\t\t\t\tnext TOKEN;\n+\t\t\t\t}\n+\n+\t\t\t\t# Following line added by rmerrell to remove '/' escape char in .netrc\n+\t\t\t\t$value =~ s/\\/\\\\/\\\\/g;\n+\t\t\t\t$mach->{$tok} = $value;\n+\t\t\t} elsif ($tok eq \"macdef\") { # we ignore macros\n+\t\t\t\tnext TOKEN unless $mach;\n+\t\t\t\tmy $value = shift @tok;\n+\t\t\t\t$macdef = 1;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treturn @entries;\n+}\n+\n+sub read_credential_data_from_stdin {\n+\t# the query: start with every token with no value\n+\tmy %q = map { $_ => undef } values(%{$options{tmap}});\n+\n+\twhile (<STDIN>) {\n+\t\tnext unless m/^([^=]+)=(.+)/;\n+\n+\t\tmy ($token, $value) = ($1, $2);\n+\t\tdie \"Unknown search token $token\" unless exists $q{$token};\n+\t\t$q{$token} = $value;\n+\t\tlog_debug(\"We were given search token $token and value $value\");\n+\t}\n+\n+\tforeach (sort keys %q) {\n+\t\tlog_debug(\"Searching for %s = %s\", $_, $q{$_} || '(any value)');\n+\t}\n+\n+\treturn \\%q;\n+}\n+\n+# takes the search tokens and then a list of entries\n+# each entry is a hash reference\n+sub find_netrc_entry {\n+\tmy $query = shift @_;\n+\n+    ENTRY:\n+\tforeach my $entry (@_)\n+\t{\n+\t\tmy $entry_text = join ', ', map { \"$_=$entry->{$_}\" } keys %$entry;\n+\t\tforeach my $check (sort keys %$query) {\n+\t\t\tif (defined $query->{$check}) {\n+\t\t\t\tlog_debug(\"compare %s [%s] to [%s] (entry: %s)\",\n+\t\t\t\t\t  $check,\n+\t\t\t\t\t  $entry->{$check},\n+\t\t\t\t\t  $query->{$check},\n+\t\t\t\t\t  $entry_text);\n+\t\t\t\tunless ($query->{$check} eq $entry->{$check}) {\n+\t\t\t\t\tnext ENTRY;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tlog_debug(\"OK: any value satisfies check $check\");\n+\t\t\t}\n+\t\t}\n+\n+\t\treturn $entry;\n+\t}\n+\n+\t# nothing was found\n+\treturn;\n+}\n+\n+sub print_credential_data {\n+\tmy $entry = shift @_;\n+\tmy $query = shift @_;\n+\n+\tlog_debug(\"entry has passed all the search checks\");\n+ TOKEN:\n+\tforeach my $git_token (sort keys %$entry) {\n+\t\tlog_debug(\"looking for useful token $git_token\");\n+\t\t# don't print unknown (to the credential helper protocol) tokens\n+\t\tnext TOKEN unless exists $query->{$git_token};\n+\n+\t\t# don't print things asked in the query (the entry matches them)\n+\t\tnext TOKEN if defined $query->{$git_token};\n+\n+\t\tlog_debug(\"FOUND: $git_token=$entry->{$git_token}\");\n+\t\tprintf \"%s=%s\\n\", $git_token, $entry->{$git_token};\n+\t}\n+}\n+sub log_verbose {\n+\treturn unless $options{verbose};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\n+\n+sub log_debug {\n+\treturn unless $options{debug};\n+\tprintf STDERR @_;\n+\tprintf STDERR \"\\n\";\n+}\ndiff --git a/contrib/credential/netrc/test.netrc b/contrib/credential/netrc/test.netrc\nnew file mode 100644\nindex 0000000..ba119a9\n--- /dev/null\n+++ b/contrib/credential/netrc/test.netrc\n@@ -0,0 +1,13 @@\n+machine imap login tzz@lifelogs.com port imaps password letmeknow\n+machine imap login bob port imaps password bobwillknow\n+\n+# comment test\n+\n+machine imap2 login tzz port 1099 password tzzknow\n+machine imap2 login bob password bobwillknow\n+\n+# another command\n+\n+machine github.com\n+  multilinetoken anothervalue\n+  login carol password carolknows\ndiff --git a/contrib/credential/netrc/test.pl b/contrib/credential/netrc/test.pl\nnew file mode 100755\nindex 0000000..169b646\n--- /dev/null\n+++ b/contrib/credential/netrc/test.pl\n@@ -0,0 +1,106 @@\n+#!/usr/bin/perl\n+\n+use warnings;\n+use strict;\n+use Test;\n+use IPC::Open2;\n+\n+BEGIN { plan tests => 15 }\n+\n+my @global_credential_args = @ARGV;\n+my $netrc = './test.netrc';\n+print \"# Testing insecure file, nothing should be found\\n\";\n+chmod 0644, $netrc;\n+my $cred = run_credential(['-f', $netrc, 'get'],\n+\t\t\t  { host => 'github.com' });\n+\n+ok(scalar keys %$cred, 0, \"Got 0 keys from insecure file\");\n+\n+print \"# Testing missing file, nothing should be found\\n\";\n+chmod 0644, $netrc;\n+$cred = run_credential(['-f', '///nosuchfile///', 'get'],\n+\t\t       { host => 'github.com' });\n+\n+ok(scalar keys %$cred, 0, \"Got 0 keys from missing file\");\n+\n+chmod 0600, $netrc;\n+\n+print \"# Testing with invalid data\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       \"bad data\");\n+ok(scalar keys %$cred, 4, \"Got first found keys with bad data\");\n+\n+print \"# Testing netrc file for a missing corovamilkbar entry\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       { host => 'corovamilkbar' });\n+\n+ok(scalar keys %$cred, 0, \"Got no corovamilkbar keys\");\n+\n+print \"# Testing netrc file for a github.com entry\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       { host => 'github.com' });\n+\n+ok(scalar keys %$cred, 2, \"Got 2 Github keys\");\n+\n+ok($cred->{password}, 'carolknows', \"Got correct Github password\");\n+ok($cred->{username}, 'carol', \"Got correct Github username\");\n+\n+print \"# Testing netrc file for a username-specific entry\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       { host => 'imap', username => 'bob' });\n+\n+ok(scalar keys %$cred, 2, \"Got 2 username-specific keys\");\n+\n+ok($cred->{password}, 'bobwillknow', \"Got correct user-specific password\");\n+ok($cred->{protocol}, 'imaps', \"Got correct user-specific protocol\");\n+\n+print \"# Testing netrc file for a host:port-specific entry\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       { host => 'imap2:1099' });\n+\n+ok(scalar keys %$cred, 2, \"Got 2 host:port-specific keys\");\n+\n+ok($cred->{password}, 'tzzknow', \"Got correct host:port-specific password\");\n+ok($cred->{username}, 'tzz', \"Got correct host:port-specific username\");\n+\n+print \"# Testing netrc file that 'host:port kills host' entry\\n\";\n+$cred = run_credential(['-f', $netrc, 'get'],\n+\t\t       { host => 'imap2' });\n+\n+ok(scalar keys %$cred, 2, \"Got 2 'host:port kills host' keys\");\n+\n+ok($cred->{password}, 'bobwillknow', \"Got correct 'host:port kills host' password\");\n+ok($cred->{username}, 'bob', \"Got correct 'host:port kills host' username\");\n+\n+sub run_credential\n+{\n+\tmy $args = shift @_;\n+\tmy $data = shift @_;\n+\tmy $pid = open2(my $chld_out, my $chld_in,\n+\t\t\t'./git-credential-netrc', @global_credential_args,\n+\t\t\t@$args);\n+\n+\tdie \"Couldn't open pipe to netrc credential helper: $!\" unless $pid;\n+\n+\tif (ref $data eq 'HASH')\n+\t{\n+\t\tprint $chld_in \"$_=$data->{$_}\\n\" foreach sort keys %$data;\n+\t}\n+\telse\n+\t{\n+\t\tprint $chld_in \"$data\\n\";\n+\t}\n+\n+\tclose $chld_in;\n+\tmy %ret;\n+\n+\twhile (<$chld_out>)\n+\t{\n+\t\tchomp;\n+\t\tnext unless m/^([^=]+)=(.+)/;\n+\n+\t\t$ret{$1} = $2;\n+\t}\n+\n+\treturn \\%ret;\n+}\n-- \n1.7.9.rc2\n"},{"id":"210251","messageId":"87621guztp.fsf@lifelogs.com","threadId":"32825","inReplyTo":"20130208061855.GA11892@sigill.intra.peff.net","subject":"Re: [PATCHv6] Add contrib/credentials/netrc with GPG support","fromName":"Ted Zlatanov","fromEmail":"tzz@lifelogs.com","sentAt":"2013-02-25T16:24:02Z","receivedAt":"2013-02-25T16:24:02Z","isPatch":false,"sender":{"key":"tzz@lifelogs.com","avatar":"https://avatars.githubusercontent.com/u/67764?v=4"},"body":"On Fri, 8 Feb 2013 01:18:55 -0500 Jeff King <peff@peff.net> wrote: \n\n>> +\t# the following check is copied from Net::Netrc, for non-GPG files\n>> +\t# OS/2 and Win32 do not handle stat in a way compatable with this check :-(\n\nJK> s/compatable/compatible/\n\nThis is from the Net::Netrc module.  Fixed in my commit but eh...\n\nJK> You mention os/2 and Win32 here, but the check has more:\n\n>> +\tunless ($gpgmode || $options{insecure} ||\n>> +\t\t$^O eq 'os2'\n>> +\t\t|| $^O eq 'MSWin32'\n>> +\t\t|| $^O eq 'MacOS'\n>> +\t\t|| $^O =~ /^cygwin/) {\n\nJK> Does MacOS really not handle stat? Or is this old MacOS, not OS X?\n\nThis is all out of Net::Netrc, and yes, it's pre-Mac OS X.  I think it's\nsafe to leave as is, but I can remove OS/2 and MacOS if you prefer.\n\nJK> So this will convert:\nJK>   machine foo port smtp\nJK> in the netrc into (protocol => \"smtp\", host => \"foo\"), but:\nJK>   machine foo port 25\nJK> into (protocol => undef, host => \"foo:25\"), right? That makes sense to\nJK> me.\n\nYes.  test.pl checks that host=foo doesn't find the above, as well.\n\nJK> I don't know if you want to take the hit of relying on Git.pm (it is\nJK> nice for the helper to be totally standalone and copy-able), but one\nJK> obvious possible refactor would be to use the credential read/write\nJK> functions recently added there. I'm OK with not doing that, though.\n\nJK> It may also be worth building on top of the regular git test harness.\nJK> It's more work, but the resulting code (and the output) will be much\nJK> more readable.\n\nAt least for now let's leave it standalone.  When and if it moves into\nthe core, we can change it to use the core's Git.pm and test suite.  The\ncode and the tests are small enough that I think using Perl's Test\nmodule makes the most sense right now.\n\nJK> Printf? Bleh, isn't this supposed to be perl? :P\n\nWhat?  Was I supposed to use formats?!?!\n\nJK> You are depending on whatever the user has in their ~/.netrc, no?\nJK> Wouldn't it make more sense to ship a sample netrc and run all of the\nJK> tests with \"-f netrc.example\"?\n\nYes.  See test.netrc.\n\nThanks\nTed\n"}]}