module GitClient class GitError < StandardError attr_reader :git_output def initialize(git_output) super @git_output = git_output end def conflict? git_output.any { |line| line.include? 'CONFLICT' } end end class Repository def initialize(repo_path) @repo_root = repo_path @git_folder = File.join(repo_path, '.git') @merge = false end def initialized? File.exists? @git_folder end def merging? return @merge end def init raise GitError.new("The folder #{@repo_root} already contains a git repository") if initialized? git 'init' end def current_branch current_branch = git('branch').split("\n").delete_if { |line| line[0] != "*" } current_branch.first.gsub("* ", "") end def add_files_to_index(files=nil) files = files ? files : '.' files = files.is_a?(Array) ? files : [files] files.each do |file| git "add \"#{file}\"" end end def workdir_clean? return status.empty? end # x index, y working_dir # X Y Meaning # ------------------------------------------------- # [MD] not updated # M [ MD] updated in index # A [ MD] added to index # D [ M] deleted from index # R [ MD] renamed in index # C [ MD] copied in index # [MARC] index and work tree matches # [ MARC] M work tree changed since index # [ MARC] D deleted in work tree # ------------------------------------------------- # D D unmerged, both deleted # A U unmerged, added by us # U D unmerged, deleted by them # U A unmerged, added by them # D U unmerged, deleted by us # A A unmerged, both added # U U unmerged, both modified # ------------------------------------------------- # ? ? untracked # ! ! ignored # ------------------------------------------------- def status output = git 'status -s' status_hash = Hash.new return status_hash if output.length == 0 || merging? output.split("\n").each do |entry| raw_status = entry[0..1] status = case raw_status ##needs to split leaving the freaking spaces on! when / (M|D)/ then :idx_not_up_to_date when /(A|M|R)M/ then :idx_not_up_to_date when /A / then :idx_added when /M / then :idx_modified when /D / then :idx_deleted when /R / then :idx_renamed when /\?\?/ then :untracked when /(DD|AA|(U|D|A)(U|D|A))/ then :merging else :unknown end filename = entry.split(' ')[1] status_hash[status] ||= [] status_hash[status] << filename end status_hash end def remotes remotes = git 'remote -v' remotes_hash = Hash.new remotes.split("\n").each do |line| split = line.split(' ')[0..1] remotes_hash[symbolicate(split[0])] = split[1] end if remotes remotes_hash end def submodules submodules_hash = Hash.new return submodules_hash unless File.exists? File.join(@repo_root, '.gitmodules') submodules = git "config -f .gitmodules --get-regexp '^submodule\..*\.path$'" submodules.split("\n").each do |line| split = line.split(' ')[0..1] submodule_name = split[0][/^submodule.(.*).path$/, 1] submodule_path = split[1] submodule_remote = (git "config -f .gitmodules --get-all 'submodule.#{submodule_name}.url'").delete "\n" submodules_hash[submodule_path] = submodule_remote end submodules_hash end def submodules_update(conf) return if submodules.empty? git "submodule init" if(conf[:init]) git "submodule update --recursive" end def branch_exist?(branch_name) git('branch').split("\n").any? { |line| line.include? branch_name } end def ensure_branch_exists(branch_name) return if branch_exist? branch_name raise GitError, "Branch #{branch_name} doesn't exists" end def ensure_workdir_clean return if workdir_clean? raise GitError, 'The working directory is not clean' end # returns the commit hash def commit(message) output = git "commit -m\"#{message}\"" output[/\[\w* (\w*)\] .*/,1] end def switch_branch(branch_name, force_create=true) ensure_branch_exists branch_name unless force_create git "branch #{branch_name}" if force_create && !branch_exist?(branch_name) git "checkout \"#{branch_name}\"" unless current_branch == branch_name end def merge(from_branch, into_branch=nil, msg=nil) ensure_branch_exists from_branch ensure_branch_exists into_branch if into_branch ensure_workdir_clean git "checkout \"#{into_branch}\"" begin merge = true msg.nil? ? git("merge --no-ff #{from_branch}"): git("merge --no-ff #{from_branch} -m \"#{msg}\"") rescue GitError => e git 'merge --abort' if e.conflict? merge = false raise e end end def save_stash git 'stash save' end def pop_stash git 'stash pop' end def rebase(from_branch, into_branch=nil) ensure_branch_exists from_branch ensure_branch_exists into_branch if into_branch ensure_workdir_clean git "checkout \"#{into_branch}\"" begin merge = true git "rebase \"#{from_branch}\"" rescue GitError => e git 'rebase --abort' if e.conflict? merge = false raise e end end private # good way of making this more robust: # https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/executable.rb # and: # https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/downloader/git.rb def git(git_cmd) output = nil FileUtils.cd(@repo_root) { output = safe_exec("git #{git_cmd}") } output end def safe_exec(cmd) cmd_output = `#{cmd} 2>&1 3>&1` raise GitError.new(cmd_output) unless $?.success? cmd_output end def symbolicate(string) string.gsub(/\s+/, "_").downcase.to_sym end end end