require "spec_helper"
require_relative "../../lib/octopolo/git"

module Octopolo
  describe Git do
    let(:cli) { stub(:CLI) }

    context ".perform(subcommand)" do
      let(:command) { "status" }

      before { Git.cli = cli }

      it "performs the given subcommand" do
        cli.should_receive(:perform).with("git #{command}")
        Git.perform command
      end
    end

    context ".perform_quietly(subcommand)" do
      let(:command) { "status" }

      before { Git.cli = cli }

      it "performs the given subcommand quietly" do
        cli.should_receive(:perform_quietly).with("git #{command}")
        Git.perform_quietly command
      end
    end

    context ".current_branch" do
      let(:name) { "foo" }
      let(:output) { "#{name}\n" }
      let(:nobranch_output) { "#{Git::NO_BRANCH}\n" }
      before { Git.cli = cli }

      it "performs a command to filter current branch from list of branches" do
        cli.should_receive(:perform_quietly).with("git branch | grep '^* ' | cut -c 3-") { output }
        Git.current_branch.should == name
      end

      it "raises NotOnBranch if not on a branch" do
        cli.should_receive(:perform_quietly) { nobranch_output }
        expect { Git.current_branch }.to raise_error(Git::NotOnBranch, "Not currently checked out to a particular branch")
      end

      it "staging and deploy should be reserved branches" do
        Git.stub(:current_branch).and_return "staging.05.12"
        Git.reserved_branch?.should be_true

        Git.stub(:current_branch).and_return "deployable.05.12"
        Git.reserved_branch?.should be_true

        Git.stub(:current_branch).and_return "qaready.05.12"
        Git.reserved_branch?.should be_true
      end

      it "other branches should not be reserved branches" do
        Git.stub(:current_branch).and_return "not_staging.05.12"
        Git.reserved_branch?.should_not be_true

        Git.stub(:current_branch).and_return "not_deployable.05.12"
        Git.reserved_branch?.should_not be_true

        Git.stub(:current_branch).and_return "not_qaready.05.12"
        Git.reserved_branch?.should_not be_true
      end
    end



    context ".check_out(branch_name)" do
      let(:name) { "foo" }

      it "checks out the given branch name" do
        Git.should_receive(:fetch)
        Git.should_receive(:perform).with("checkout #{name}")
        Git.should_receive(:pull)
        Git.should_receive(:current_branch) { name }
        Git.check_out name
      end

      it "raises an exception if the current branch is not the requested branch afterward" do
        Git.should_receive(:fetch)
        Git.should_receive(:perform)
        Git.should_receive(:pull)
        Git.should_receive(:current_branch) { "other" }
        expect { Git.check_out name }.to raise_error(Git::CheckoutFailed, "Failed to check out '#{name}'")
      end
    end

    context ".clean?" do
      let(:cmd) { "git status --short" }

      before { Git.cli = cli }

      it "returns true if everything is checked in" do
        cli.should_receive(:perform_quietly).with(cmd) { "" }
        Git.should be_clean
      end

      it "returns false if the index has untracked files" do
        cli.should_receive(:perform_quietly).with(cmd) { "?? foo.txt" }
        Git.should_not be_clean
      end

      it "returns false if the index has missing files" do
        cli.should_receive(:perform_quietly).with(cmd) { "D foo.txt" }
        Git.should_not be_clean
      end

      it "returns false if the index has changed files" do
        cli.should_receive(:perform_quietly).with(cmd) { "M foo.txt" }
        Git.should_not be_clean
      end
    end

    context ".if_clean" do
      let(:custom_message) { "Some other message" }

      it "performs the block if the git index is clean" do
        Git.should_receive(:clean?) { true }
        Math.should_receive(:log).with(1)

        Git.if_clean do
          Math.log 1
        end
      end

      it "does not perform the block if the git index is not clean" do
        Git.should_receive(:clean?) { false }
        Math.should_not_receive(:log)
        Git.should_receive(:alert_dirty_index).with(Git::DEFAULT_DIRTY_MESSAGE)

        Git.if_clean do
          Math.log 1
        end
      end

      it "prints a custom message if git index is not clean" do
        Git.should_receive(:clean?) { false }
        Math.should_not_receive(:log)
        Git.should_receive(:alert_dirty_index).with(custom_message)

        Git.if_clean custom_message do
          Math.log 1
        end
      end
    end

    context ".alert_dirty_index(message)" do
      let(:message) { "Some message" }

      before { Git.cli = cli }

      it "prints the given message and shows the git status" do
        cli.should_receive(:say).with(" ")
        cli.should_receive(:say).with(message)
        cli.should_receive(:say).with(" ")
        Git.should_receive(:perform).with("status")

        Git.alert_dirty_index message
      end
    end

    context ".merge(branch_name)" do
      let(:branch_name) { "foo" }

      it "fetches the latest code and merges the given branch name" do
        Git.should_receive(:if_clean).and_yield
        Git.should_receive(:fetch)
        Git.should_receive(:perform).with("merge --no-ff origin/#{branch_name}")
        Git.should_receive(:clean?) { true }
        Git.should_receive(:push)

        Git.merge branch_name
      end

      it "does not push and raises MergeFailed if the merge failed" do
        Git.should_receive(:if_clean).and_yield
        Git.should_receive(:fetch)
        Git.should_receive(:perform).with("merge --no-ff origin/#{branch_name}")
        Git.should_receive(:clean?) { false }
        Git.should_not_receive(:push)

        expect { Git.merge branch_name }.to raise_error(Git::MergeFailed)
      end
    end

    context ".fetch" do
      it "fetches and prunes remote branches" do
        Git.should_receive(:perform_quietly).with("fetch --prune")

        Git.fetch
      end
    end

    context ".push" do
      let(:branch) { "current_branch" }

      it "pushes the current branch" do
        Git.stub(current_branch: branch)
        Git.should_receive(:if_clean).and_yield
        Git.should_receive(:perform).with("push origin #{branch}")

        Git.push
      end
    end

    context ".pull" do
      it "performs a pull if the index is clean" do
        Git.should_receive(:if_clean).and_yield
        Git.should_receive(:perform).with("pull")
        Git.pull
      end
    end

    context ".remote_branches" do
      let(:raw_output) { raw_names.join("\n  ") }
      let(:raw_names) { %w(origin/foo origin/bar) }
      let(:cleaned_names) { %w(foo bar) }

      it "prunes the remote branch list and grabs all the branch names" do
        Git.should_receive(:fetch)
        Git.should_receive(:perform_quietly).with("branch --remote") { raw_output }
        Git.remote_branches.should == cleaned_names.sort
      end
    end

    context ".branches_for branch_type" do
      let(:remote_branches) { [depl1, rando, stage1, depl2].sort }
      let(:depl1) { "deployable.12.20" }
      let(:depl2) { "deployable.11.05" }
      let(:stage1) { "staging.04.05" }
      let(:rando) { "something-else" }

      before do
        Git.should_receive(:remote_branches) { remote_branches }
      end

      it "can find deployable branches" do
        deployables = Git.branches_for(Git::DEPLOYABLE_PREFIX)
        deployables.should include depl1
        deployables.should include depl2
        deployables.should == [depl1, depl2].sort
        deployables.count.should == 2
      end

      it "can find staging branches" do
        stagings = Git.branches_for(Git::STAGING_PREFIX)
        stagings.should include stage1
        stagings.count.should == 1
      end
    end

    context ".deployable_branch" do
      let(:depl1) { "deployable.12.05" }
      let(:depl2) { "deployable.12.25" }

      it "returns the last deployable branch" do
        Git.should_receive(:branches_for).with(Git::DEPLOYABLE_PREFIX) { [depl1, depl2] }
        Git.deployable_branch.should == depl2
      end

      it "raises an exception if none exist" do
        Git.should_receive(:branches_for).with(Git::DEPLOYABLE_PREFIX) { [] }
        expect { Git.deployable_branch.should }.to raise_error(Git::NoBranchOfType, "No #{Git::DEPLOYABLE_PREFIX} branch")
      end
    end

    context ".staging_branch" do
      let(:stage1) { "stage1" }
      let(:stage2) { "stage2" }

      it "returns the last staging branch" do
        Git.should_receive(:branches_for).with(Git::STAGING_PREFIX) { [stage1, stage2] }
        Git.staging_branch.should == stage2
      end

      it "raises an exception if none exist" do
        Git.should_receive(:branches_for).with(Git::STAGING_PREFIX) { [] }
        expect { Git.staging_branch}.to raise_error(Git::NoBranchOfType, "No #{Git::STAGING_PREFIX} branch")
      end
    end

    context ".qaready_branch" do
      let(:qaready1) { "qaready1" }
      let(:qaready2) { "qaready2" }

      it "returns the last qaready branch" do
        Git.should_receive(:branches_for).with(Git::QAREADY_PREFIX) { [qaready1, qaready2] }
        Git.qaready_branch.should == qaready2
      end

      it "raises an exception if none exist" do
        Git.should_receive(:branches_for).with(Git::QAREADY_PREFIX) { [] }
        expect { Git.qaready_branch }.to raise_error(Git::NoBranchOfType, "No #{Git::QAREADY_PREFIX} branch")
      end
    end

    context ".release_tags" do
      let(:valid1) { "2012.02.28" }
      let(:valid2) { "2012.11.10" }
      let(:invalid) { "foothing" }
      let(:tags) { [valid1, invalid, valid2].join("\n") }

      it "returns all the tags for releases" do
        Git.should_receive(:perform_quietly).with("tag") { tags }
        release_tags = Git.release_tags
        release_tags.should_not include invalid
        release_tags.should include valid1
        release_tags.should include valid2
      end
    end

    context ".recent_release_tags" do
      let(:long_list) { Array.new(100, "sometag#{rand(1000)}") } # big-ass list

      it "returns the last #{Git::RECENT_TAG_LIMIT} tags" do
        Git.should_receive(:release_tags) { long_list }
        tags = Git.recent_release_tags
        tags.count.should == Git::RECENT_TAG_LIMIT
        tags.should == long_list.last(Git::RECENT_TAG_LIMIT)
      end
    end

    context ".new_branch(new_branch_name, source_branch_name)" do
      let(:new_branch_name) { "foo" }
      let(:source_branch_name) { "bar" }

      it "creates and pushes a new branch from the source branch" do
        Git.should_receive(:fetch)
        Git.should_receive(:perform).with("branch --no-track #{new_branch_name} origin/#{source_branch_name}")
        Git.should_receive(:check_out).with(new_branch_name)
        Git.should_receive(:perform).with("push --set-upstream origin #{new_branch_name}")

        Git.new_branch(new_branch_name, source_branch_name)
      end
    end

    context ".new_tag(tag_name)" do
      let(:tag) { "asdf" }

      it "creates a new tag with the given name and pushes it" do
        Git.should_receive(:perform).with("tag #{tag}")
        Git.should_receive(:push)
        Git.should_receive(:perform).with("push --tag")

        Git.new_tag(tag)
      end
    end

    context ".stale_branches(destination_branch, branches_to_ignore)" do
      let(:ignored) { %w(foo bar) }
      let(:branch_name) { "master" }
      let(:sha) { "asdf123" }
      let(:raw_result) do
        %Q(
          origin/bing
          origin/bang
        )
      end

      it "checks for stale branches for the given branch, less branches to ignore" do
        Git.should_receive(:fetch)
        Git.should_receive(:stale_branches_to_ignore).with(ignored) { ignored }
        Git.should_receive(:recent_sha).with(branch_name) { sha }
        Git.should_receive(:perform_quietly).with("branch --remote --merged #{sha} | grep -E -v '(foo|bar)'") { raw_result }

        expect(Git.stale_branches(branch_name, ignored)).to eq(%w(bing bang))
      end

      it "defaults to master branch and no extra branches to ignore" do
        Git.should_receive(:fetch)
        Git.should_receive(:stale_branches_to_ignore).with([]) { ignored }
        Git.should_receive(:recent_sha).with("master") { sha }
        Git.should_receive(:perform_quietly).with("branch --remote --merged #{sha} | grep -E -v '(foo|bar)'") { raw_result }

        Git.stale_branches
      end
    end

    context "#branches_to_ignore(custom_branch_list)" do
      it "ignores some branches by default" do
        expect(Git.send(:stale_branches_to_ignore)).to include "HEAD"
        expect(Git.send(:stale_branches_to_ignore)).to include "master"
        expect(Git.send(:stale_branches_to_ignore)).to include "staging"
        expect(Git.send(:stale_branches_to_ignore)).to include "deployable"
      end

      it "accepts an optional list of additional branches to ignore" do
        expect(Git.send(:stale_branches_to_ignore, ["foo"])).to include "HEAD"
        expect(Git.send(:stale_branches_to_ignore, ["foo"])).to include "master"
        expect(Git.send(:stale_branches_to_ignore, ["foo"])).to include "staging"
        expect(Git.send(:stale_branches_to_ignore, ["foo"])).to include "deployable"
        expect(Git.send(:stale_branches_to_ignore, ["foo"])).to include "foo"
      end
    end

    context "#recent_sha(branch_name)" do
      let(:branch_name) { "foo" }
      let(:raw_sha) { "asdf123\n" }

      it "grabs the SHA of the given branch from 1 day ago" do
        Git.should_receive(:perform_quietly).with("rev-list `git rev-parse remotes/origin/#{branch_name} --before=1.day.ago` --max-count=1") { raw_sha }
        expect(Git.send(:recent_sha, branch_name)).to eq("asdf123")
      end
    end

    context ".delete_branch(branch_name)" do
      let(:branch_name) { "foo" }

      it "leverages git-extra's delete-branch command" do
        Git.should_receive(:perform).with("push origin :#{branch_name}")
        Git.should_receive(:perform).with("branch -D #{branch_name}")
        Git.delete_branch branch_name
      end
    end
  end
end