Skip to content
2 changes: 2 additions & 0 deletions bundler/lib/bundler/man/bundle-config.1
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ The following is a list of all configuration keys and their purpose\. You can le
.IP "\(bu" 4
\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\.
.IP "\(bu" 4
\fBcredential\.helper\fR (\fBBUNDLE_CREDENTIAL_HELPER\fR): The path to a credential helper to use for fetching credentials from a remote gem server\.
.IP "\(bu" 4
\fBdefault_install_uses_path\fR (\fBBUNDLE_DEFAULT_INSTALL_USES_PATH\fR): Whether a \fBbundle install\fR without an explicit \fB\-\-path\fR argument defaults to installing gems in \fB\.bundle\fR\.
.IP "\(bu" 4
\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Disallow changes to the \fBGemfile\fR\. When the \fBGemfile\fR is changed and the lockfile has not been updated, running Bundler commands will be blocked\.
Expand Down
3 changes: 3 additions & 0 deletions bundler/lib/bundler/man/bundle-config.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html).
`bundle install`.
* `console` (`BUNDLE_CONSOLE`):
The console that `bundle console` starts. Defaults to `irb`.
* `credential.helper` (`BUNDLE_CREDENTIAL_HELPER`):
The path to a credential helper to use for fetching credentials from a
remote gem server.
* `default_install_uses_path` (`BUNDLE_DEFAULT_INSTALL_USES_PATH`):
Whether a `bundle install` without an explicit `--path` argument defaults
to installing gems in `.bundle`.
Expand Down
29 changes: 28 additions & 1 deletion bundler/lib/bundler/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class Settings
system_bindir
trust-policy
version
credential.helper
].freeze

DEFAULT_CONFIG = {
Expand Down Expand Up @@ -197,7 +198,7 @@ def mirror_for(uri)
end

def credentials_for(uri)
self[uri.to_s] || self[uri.host]
credentials_from_helper(uri) || self[uri.to_s] || self[uri.host]
end

def gem_mirrors
Expand Down Expand Up @@ -595,5 +596,31 @@ def self.key_to_s(key)
end
end
end

def credentials_from_helper(uri)
helper_key = "credential.helper.#{uri.host}"
helper_path = self[helper_key]
return unless helper_path

begin
require "shellwords"
command = Shellwords.shellsplit(helper_path)
command[0] = if command[0].start_with?("/", "~")
command[0]
else
"bundler-credential-#{command[0]}"
end

output = Bundler.with_unbundled_env { IO.popen(command, &:read) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - lets check $? after IO.popen

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, db1e5ab fixed checking Process.last_status (use $? difficult to write specs)

output = output.to_s.strip
output.empty? ? nil : output
rescue Errno::ENOENT, ArgumentError => e
Bundler.ui.warn "Credential helper #{helper_path} not available: #{e.message}"
nil
rescue StandardError => e
Bundler.ui.warn "Credential helper failed: #{e.message}"
nil
end
end
end
end
35 changes: 35 additions & 0 deletions bundler/spec/bundler/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,41 @@
expect(settings.credentials_for(uri)).to eq(credentials)
end
end

context "with credential helper configured" do
let(:helper_path) { "/path/to/helper" }
let(:uri) { Gem::URI("https://gemserver.example.org") }

before do
settings.set_local "credential.helper.gemserver.example.org", helper_path
end

it "uses the credential helper when configured" do
expect(IO).to receive(:popen).with([helper_path]).and_yield(StringIO.new("username:password\n"))
expect(settings.credentials_for(uri)).to eq("username:password")
end

it "fallback to config when helper fails" do
expect(IO).to receive(:popen).with([helper_path]).and_raise(StandardError, "Helper failed")
expect(Bundler.ui).to receive(:warn).with("Credential helper failed: Helper failed")
settings.set_local "gemserver.example.org", "fallback:password"
expect(settings.credentials_for(uri)).to eq("fallback:password")
end

it "returns nil when helper fails and no fallback config exists" do
expect(IO).to receive(:popen).with([helper_path]).and_yield(StringIO.new(""))
expect(settings.credentials_for(uri)).to be_nil
end

context "with relative helper path and options" do
let(:helper_path) { "custom-helper --foo=bar" }

it "prepends bundler-credential- to the helper name" do
expect(IO).to receive(:popen).with(["bundler-credential-custom-helper", "--foo=bar"]).and_yield(StringIO.new("username:password\n"))
expect(settings.credentials_for(uri)).to eq("username:password")
end
end
end
end

describe "URI normalization" do
Expand Down
Loading