require 'rubygems' require 'fileutils' require 'uri' require 'yaml' require 'httpclient' require 'httpi' gem 'launchy', '= 2.0.5' require 'launchy' gem 'yajl-ruby', '= 1.1.0' require 'yajl' $:.unshift File.dirname(__FILE__) require "ext/string" require "gitcycle/assist" require "gitcycle/branch" require "gitcycle/checkout" require "gitcycle/commit" require "gitcycle/discuss" require "gitcycle/incident" require "gitcycle/open" require "gitcycle/pull" require "gitcycle/push" require "gitcycle/qa" require "gitcycle/ready" require "gitcycle/review" require "gitcycle/setup" class Gitcycle API = if ENV['ENV'] == 'test' "http://127.0.0.1:3000/api" else "http://gitcycle.bleacherreport.com/api" end ERROR = { :unrecognized_url => 1, :could_not_find_branch => 2, :told_not_to_merge => 3, :cannot_qa => 4, :conflict_when_merging => 5, :something_went_wrong => 6, :git_origin_not_found => 7, :last_command_errored => 8, } include Assist include Branch include Checkout include Commit include Discuss include Incident include Open include Pull include Push include QA include Ready include Review include Setup def initialize(args=nil) $remotes = {} if ENV['CONFIG'] @config_path = File.expand_path(ENV['CONFIG']) else @config_path = File.expand_path("~/.gitcycle.yml") end load_config load_git start(args) if args end def start(args=[]) command = args.shift `git --help`.scan(/\s{3}(\w+)\s{3}/).flatten.each do |cmd| if command == cmd && !self.respond_to?(command) exec_git(cmd, args) end end if command.nil? puts "\nNo command specified\n".red elsif command =~ /^-/ command_not_recognized elsif self.respond_to?(command) send(command, *args) else command_not_recognized end end private def add_remote_and_fetch(options={}) owner = options[:owner] repo = options[:repo] unless $remotes[owner] $remotes[owner] = true unless remotes(:match => owner) puts "Adding remote repo '#{owner}/#{repo}'.\n".green run("git remote add #{owner} git@github.com:#{owner}/#{repo}.git") end puts "Fetching remote '#{owner}'.\n".green run("git fetch -q #{owner}", :catch => options[:catch]) end end def branches(options={}) b = `git branch#{" -a" if options[:all]}#{" -r" if options[:remote]}` if options[:current] b.match(/\*\s+(.+)/)[1] elsif options[:match] b.match(/([\s]+|origin\/)(#{options[:match]})$/)[2] rescue nil elsif options[:array] b.split(/\n/).map{|b| b[2..-1]} else b end end def checkout_or_track(options={}) name = options[:name] remote = options[:remote] if branches(:match => name) puts "Checking out branch '#{name}'.\n".green run("git checkout #{name} -q") else puts "Tracking branch '#{remote}/#{name}'.\n".green run("git fetch -q #{remote}") run("git checkout -q -b #{name} #{remote}/#{name}") end run("git pull #{remote} #{name} -q") end def checkout_remote_branch(options={}) owner = options[:owner] repo = options[:repo] branch = options[:branch] target = options[:target] || branch if branches(:match => target) if yes?("You already have a branch called '#{target}'. Overwrite?") run("git push origin :#{target} -q") run("git checkout master -q") run("git branch -D #{target}") else run("git checkout #{target} -q") run("git pull origin #{target} -q") return end end add_remote_and_fetch(options) puts "Checking out remote branch '#{target}' from '#{owner}/#{repo}/#{branch}'.\n".green run("git checkout -q -b #{target} #{owner}/#{branch}") puts "Fetching remote 'origin'.\n".green run("git fetch -q origin") if branches(:remote => true, :match => "origin/#{target}") puts "Pulling 'origin/#{target}'.\n".green run("git pull origin #{target} -q") end puts "Pushing 'origin/#{target}'.\n".green run("git push origin #{target} -q") end def command_not_recognized readme = "https://github.com/winton/gitcycle/blob/master/README.md" puts "\nCommand not recognized.".red puts "\nOpening #{readme}\n".green Launchy.open(readme) end def create_pull_request(branch=nil, force=false) unless branch puts "\nRetrieving branch information from gitcycle.\n".green branch = get('branch', 'branch[name]' => branches(:current => true), 'create' => 0 ) end if branch && (force || !branch['issue_url']) puts "Creating GitHub pull request.\n".green branch = get('branch', 'branch[create_pull_request]' => true, 'branch[name]' => branch['name'], 'create' => 0 ) end branch end def errored?(output) output.include?("fatal: ") || output.include?("ERROR: ") || $?.exitstatus != 0 end def exec_git(command, args) args.unshift("git", command) Kernel.exec(*args.collect(&:to_s)) end def fix_conflict(options) owner = options[:owner] repo = options[:repo] branch = options[:branch] issue = options[:issue] issues = options[:issues] type = options[:type] if $? != 0 puts "Conflict occurred when merging '#{branch}'#{" (issue ##{issue})" if issue}.\n".red if type == :to_qa puts "Please resolve this conflict with '#{owner}'.\n".yellow puts "\nSending conflict information to gitcycle.\n".green get('qa_branch', 'issues' => issues, "conflict_#{type}" => issue) puts "Type 'gitc qa resolved' when finished resolving.\n".yellow exit ERROR[:conflict_when_merging] end elsif type # from_qa or to_qa branch = branches(:current => true) puts "Pushing branch '#{branch}'.\n".green run("git push origin #{branch} -q") end end def get(path, hash={}) hash.merge!( :login => @login, :token => @token, :uid => (0...20).map{ ('a'..'z').to_a[rand(26)] }.join ) hash[:test] = 1 if ENV['ENV'] == 'test' puts "Transaction ID: #{hash[:uid]}".green params = '' hash[:session] = 0 hash.each do |k, v| if v && v.is_a?(::Array) params << "#{URI.escape(k.to_s)}=#{URI.escape(v.inspect)}&" elsif v params << "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}&" end end params.chop! # trailing & begin HTTPI.log = false req = HTTPI::Request.new "#{API}/#{path}.json?#{params}" json = HTTPI.get(req).body rescue Exception => error puts error.to_s puts "\nCould not connect to Gitcycle.".red puts "\nPlease verify your Internet connection and try again later.\n".yellow exit end match = json.match(/Gitcycle error reference code (\d+)/) error = match && match[1] if error puts "\nSomething went wrong :(".red puts "\nEmail error code #{error} to wwelsh@bleacherreport.com.".yellow puts "\nInclude a gist of your terminal output if possible.\n".yellow exit ERROR[:something_went_wrong] else Yajl::Parser.parse(json) end end def git_config_path(path) config = "#{path}/.git/config" if File.exists?(config) return config elsif path == '/' return nil else path = File.expand_path(path + '/..') git_config_path(path) end end def load_config if File.exists?(@config_path) @config = YAML.load(File.read(@config_path)) else @config = {} end end def load_git path = git_config_path(Dir.pwd) if path @git_url = File.read(path).match(/\[remote "origin"\][^\[]*url = ([^\n]+)/m)[1] @git_repo = @git_url.match(/([^\/]+)\.git/)[1] @git_login = @git_url.match(/([^\/:]+)\/[^\/]+\.git/)[1] @login, @token = @config["#{@git_login}/#{@git_repo}"] rescue [ nil, nil ] end end def merge_remote_branch(options={}) owner = options[:owner] repo = options[:repo] branch = options[:branch] add_remote_and_fetch(options) if branches(:remote => true, :match => "#{owner}/#{branch}") puts "\nMerging remote branch '#{branch}' from '#{owner}/#{repo}'.\n".green run("git merge #{owner}/#{branch}") fix_conflict(options) end end def options?(args) args.any? { |arg| arg =~ /^-/ } end def q(question, extra='') puts "#{question.yellow}#{extra}" $input ? $input.shift : $stdin.gets.strip end def remotes(options={}) b = `git remote` if options[:match] b.match(/^(#{options[:match]})$/)[1] rescue nil else b end end def require_config unless @login && @token puts "\nGitcycle configuration not found.".red puts "Are you in the right repository?".yellow puts "Have you set up this repository at http://gitcycle.com?\n".yellow exit end true end def require_git unless @git_url && @git_repo && @git_login puts "\norigin entry within '.git/config' not found!".red puts "Are you sure you are in a git repository?\n".yellow exit ERROR[:git_origin_not_found] end true end def run(cmd, options={}) if ENV['RUN'] == '0' puts cmd else output = `#{cmd} 2>&1` end if options[:catch] != false && errored?(output) puts "#{output}\n\n" puts "Gitcycle encountered an error when running the last command:".red puts " #{cmd}\n" puts "Please copy this session's output and send it to gitcycle@bleacherreport.com.\n".yellow exit ERROR[:last_command_errored] else output end end def save_config FileUtils.mkdir_p(File.dirname(@config_path)) File.open(@config_path, 'w') do |f| f.write(YAML.dump(@config)) end end def yes?(question) q(question, " (#{"y".green}/#{"n".red})").downcase[0..0] == 'y' end end