From c16dc5c9c9b2baaf11b8cc48f59763516ca150a8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 13 Nov 2014 17:07:54 -0500 Subject: [PATCH] Add heroku push implementation --- plugins/pushes/heroku/errors.rb | 21 ++ plugins/pushes/heroku/push.rb | 110 ++++++++ test/unit/plugins/pushes/heroku/push_test.rb | 279 +++++++++++++++++++ 3 files changed, 410 insertions(+) create mode 100644 plugins/pushes/heroku/errors.rb create mode 100644 plugins/pushes/heroku/push.rb create mode 100644 test/unit/plugins/pushes/heroku/push_test.rb diff --git a/plugins/pushes/heroku/errors.rb b/plugins/pushes/heroku/errors.rb new file mode 100644 index 000000000..c92a7b76c --- /dev/null +++ b/plugins/pushes/heroku/errors.rb @@ -0,0 +1,21 @@ +module VagrantPlugins + module HerokuPush + module Errors + class Error < Vagrant::Errors::VagrantError + error_namespace("heroku_push.errors") + end + + class CommandFailed < Error + error_key(:command_failed) + end + + class GitNotFound < Error + error_key(:git_not_found) + end + + class NotAGitRepo < Error + error_key(:not_a_git_repo) + end + end + end +end diff --git a/plugins/pushes/heroku/push.rb b/plugins/pushes/heroku/push.rb new file mode 100644 index 000000000..8f50c4580 --- /dev/null +++ b/plugins/pushes/heroku/push.rb @@ -0,0 +1,110 @@ +require "vagrant/util/safe_exec" +require "vagrant/util/subprocess" +require "vagrant/util/which" + +require_relative "errors" + +module VagrantPlugins + module HerokuPush + class Push < Vagrant.plugin("2", :push) + def push + # Expand any paths relative to the root + dir = File.expand_path(config.dir, env.root_path) + + # Verify git is installed + verify_git_bin!(config.git_bin) + + # Verify we are operating in a git repo + verify_git_repo!(dir) + + # Check if we need to add the git remote + if !has_git_remote?(config.remote, dir) + add_heroku_git_remote(config.remote, config.app, dir) + end + + # Push to Heroku + git_push_heroku(config.remote, config.branch, dir) + end + + # Verify that git is installed. + # @raise [Errors::GitNotFound] + def verify_git_bin!(path) + if Vagrant::Util::Which.which(path).nil? + raise Errors::GitNotFound, bin: path + end + end + + # Verify that the given path is a git directory. + # @raise [Errors::NotAGitRepo] + # @param [String] + def verify_git_repo!(path) + if !File.directory?(git_dir(path)) + raise Errors::NotAGitRepo, path: path + end + end + + # The git directory for the given path. + # @param [String] path + # @return [String] + def git_dir(path) + "#{path}/.git" + end + + # Push to the Heroku remote. + # @param [String] remote + # @param [String] branch + def git_push_heroku(remote, branch, path) + execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "push", remote, branch, + ) + end + + # Check if the git remote has the given remote. + # @param [String] remote + # @return [true, false] + def has_git_remote?(remote, path) + result = execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "remote", + ) + remotes = result.stdout.split(/\r?\n/).map(&:strip) + remotes.include?(remote.to_s) + end + + # Add the Heroku to the current repository. + # @param [String] remote + # @param [String] app + def add_heroku_git_remote(remote, app, path) + execute!("git", + "--git-dir", git_dir(path), + "--work-tree", path, + "remote", "add", remote, heroku_git_url(app), + ) + end + + # The URL for this project on Heroku. + # @return [String] + def heroku_git_url(app) + "git@heroku.com:#{app}.git" + end + + # Execute the command, raising an exception if it fails. + # @return [Vagrant::Util::Subprocess::Result] + def execute!(*cmd) + result = Vagrant::Util::Subprocess.execute(*cmd) + + if result.exit_code != 0 + raise Errors::CommandFailed, + cmd: cmd.join(" "), + stdout: result.stdout, + stderr: result.stderr + end + + result + end + end + end +end diff --git a/test/unit/plugins/pushes/heroku/push_test.rb b/test/unit/plugins/pushes/heroku/push_test.rb new file mode 100644 index 000000000..1bec15927 --- /dev/null +++ b/test/unit/plugins/pushes/heroku/push_test.rb @@ -0,0 +1,279 @@ +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/pushes/heroku/push") + +describe VagrantPlugins::HerokuPush::Push do + include_context "unit" + + before(:all) do + I18n.load_path << Vagrant.source_root.join("plugins/pushes/heroku/locales/en.yml") + I18n.reload! + end + + let(:env) { isolated_environment } + let(:config) do + double("config", + app: "bacon", + dir: "lib", + git_bin: "git", + remote: "heroku", + branch: "master", + ) + end + + subject { described_class.new(env, config) } + + describe "#push" do + let(:root_path) { "/handy/dandy" } + let(:dir) { "#{root_path}/#{config.dir}" } + + before do + allow(subject).to receive(:verify_git_bin!) + allow(subject).to receive(:verify_git_repo!) + allow(subject).to receive(:has_git_remote?) + allow(subject).to receive(:add_heroku_git_remote) + allow(subject).to receive(:git_push_heroku) + allow(subject).to receive(:execute!) + + allow(env).to receive(:root_path) + .and_return(root_path) + end + + it "verifies the git bin is present" do + expect(subject).to receive(:verify_git_bin!) + .with(config.git_bin) + subject.push + end + + it "verifies the directory is a git repo" do + expect(subject).to receive(:verify_git_repo!) + .with(dir) + subject.push + end + + context "when the heroku remote exists" do + before do + allow(subject).to receive(:has_git_remote?) + .and_return(true) + end + + it "does not add the heroku remote" do + expect(subject).to_not receive(:add_heroku_git_remote) + subject.push + end + end + + context "when the heroku remote does not exist" do + before do + allow(subject).to receive(:has_git_remote?) + .and_return(false) + end + + it "adds the heroku remote" do + expect(subject).to receive(:add_heroku_git_remote) + .with(config.remote, config.app, dir) + subject.push + end + end + + it "pushes to heroku" do + expect(subject).to receive(:git_push_heroku) + .with(config.remote, config.branch, dir) + subject.push + end + end + + describe "#verify_git_bin!" do + context "when git does not exist" do + before do + allow(Vagrant::Util::Which).to receive(:which) + .with("git") + .and_return(nil) + end + + it "raises an exception" do + expect { + subject.verify_git_bin!("git") + } .to raise_error(VagrantPlugins::HerokuPush::Errors::GitNotFound) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.git_not_found", + bin: "git", + )) + } + end + end + + context "when git exists" do + before do + allow(Vagrant::Util::Which).to receive(:which) + .with("git") + .and_return("git") + end + + it "does not raise an exception" do + expect { subject.verify_git_bin!("git") }.to_not raise_error + end + end + end + + describe "#verify_git_repo!" do + context "when the path is a git repo" do + before do + allow(File).to receive(:directory?) + .with("/repo/path/.git") + .and_return(false) + end + + it "raises an exception" do + expect { + subject.verify_git_repo!("/repo/path") + } .to raise_error(VagrantPlugins::HerokuPush::Errors::NotAGitRepo) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.not_a_git_repo", + path: "/repo/path", + )) + } + end + end + + context "when the path is not a git repo" do + before do + allow(File).to receive(:directory?) + .with("/repo/path/.git") + .and_return(true) + end + + it "does not raise an exception" do + expect { subject.verify_git_repo!("/repo/path") }.to_not raise_error + end + end + end + + describe "#git_push_heroku" do + let(:dir) { "." } + + before { allow(subject).to receive(:execute!) } + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "push", "bacon", "hamlet", + ) + subject.git_push_heroku("bacon", "hamlet", dir) + end + end + + describe "#has_git_remote?" do + let(:dir) { "." } + + let(:process) do + double("process", + stdout: "origin\r\nbacon\nhello" + ) + end + + before do + allow(subject).to receive(:execute!) + .and_return(process) + end + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "remote", + ) + subject.has_git_remote?("bacon", dir) + end + + it "returns true when the remote exists" do + expect(subject.has_git_remote?("origin", dir)).to be(true) + expect(subject.has_git_remote?("bacon", dir)).to be(true) + expect(subject.has_git_remote?("hello", dir)).to be(true) + end + + it "returns false when the remote does not exist" do + expect(subject.has_git_remote?("nope", dir)).to be(false) + end + end + + describe "#add_heroku_git_remote" do + let(:dir) { "." } + + before do + allow(subject).to receive(:execute!) + allow(subject).to receive(:heroku_git_url) + .with("app") + .and_return("HEROKU_URL") + end + + it "executes the proper command" do + expect(subject).to receive(:execute!) + .with("git", + "--git-dir", "#{dir}/.git", + "--work-tree", dir, + "remote", "add", "bacon", "HEROKU_URL", + ) + subject.add_heroku_git_remote("bacon", "app", dir) + end + end + + describe "#heroku_git_url" do + it "returns the proper string" do + expect(subject.heroku_git_url("bacon")) + .to eq("git@heroku.com:bacon.git") + end + end + + describe "#git_dir" do + it "returns the .git directory for the path" do + expect(subject.git_dir("/path")).to eq("/path/.git") + end + end + + describe "#execute!" do + let(:exit_code) { 0 } + let(:stdout) { "This is the output" } + let(:stderr) { "This is the errput" } + + let(:process) do + double("process", + exit_code: exit_code, + stdout: stdout, + stderr: stderr, + ) + end + + before do + allow(Vagrant::Util::Subprocess).to receive(:execute) + .and_return(process) + end + + it "creates a subprocess" do + expect(Vagrant::Util::Subprocess).to receive(:execute) + expect { subject.execute! }.to_not raise_error + end + + it "returns the resulting process" do + expect(subject.execute!).to be(process) + end + + context "when the exit code is non-zero" do + let(:exit_code) { 1 } + + it "raises an exception" do + klass = VagrantPlugins::HerokuPush::Errors::CommandFailed + cmd = ["foo", "bar"] + + expect { subject.execute!(*cmd) }.to raise_error(klass) { |error| + expect(error.message).to eq(I18n.t("heroku_push.errors.command_failed", + cmd: cmd.join(" "), + stdout: stdout, + stderr: stderr, + )) + } + end + end + end +end