require 'rchardet' require 'tempfile' require 'zlib' module Git class GitExecuteError < StandardError end class Lib @@semaphore = Mutex.new # The path to the Git working copy. The default is '"./.git"'. # # @return [Pathname] the path to the Git working copy. # # @see [Git working tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree) # attr_reader :git_work_dir # The path to the Git repository directory. The default is # `"#{git_work_dir}/.git"`. # # @return [Pathname] the Git repository directory. # # @see [Git repository](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefrepositoryarepository) # attr_reader :git_dir # The Git index file used to stage changes (using `git add`) before they # are committed. # # @return [Pathname] the Git index file # # @see [Git index file](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefindexaindex) # attr_reader :git_index_file # Create a new Git::Lib object # # @param [Git::Base, Hash] base An object that passes in values for # @git_work_dir, @git_dir, and @git_index_file # # @param [Logger] logger # # @option base [Pathname] :working_directory # @option base [Pathname] :repository # @option base [Pathname] :index # def initialize(base = nil, logger = nil) @git_dir = nil @git_index_file = nil @git_work_dir = nil @path = nil if base.is_a?(Git::Base) @git_dir = base.repo.path @git_index_file = base.index.path if base.index @git_work_dir = base.dir.path if base.dir elsif base.is_a?(Hash) @git_dir = base[:repository] @git_index_file = base[:index] @git_work_dir = base[:working_directory] end @logger = logger end # creates or reinitializes the repository # # options: # :bare # :working_directory # :initial_branch # def init(opts={}) arr_opts = [] arr_opts << '--bare' if opts[:bare] arr_opts << "--initial-branch=#{opts[:initial_branch]}" if opts[:initial_branch] command('init', arr_opts) end # tries to clone the given repo # # accepts options: # :bare:: no working directory # :branch:: name of branch to track (rather than 'master') # :depth:: the number of commits back to pull # :origin:: name of remote (same as remote) # :path:: directory where the repo will be cloned # :remote:: name of remote (rather than 'origin') # :recursive:: after the clone is created, initialize all submodules within, using their default settings. # # TODO - make this work with SSH password or auth_key # # @return [Hash] the options to pass to {Git::Base.new} # def clone(repository, name, opts = {}) @path = opts[:path] || '.' clone_dir = opts[:path] ? File.join(@path, name) : name arr_opts = [] arr_opts << '--bare' if opts[:bare] arr_opts << '--branch' << opts[:branch] if opts[:branch] arr_opts << '--depth' << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0 arr_opts << '--config' << opts[:config] if opts[:config] arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin] arr_opts << '--recursive' if opts[:recursive] arr_opts << "--mirror" if opts[:mirror] arr_opts << '--' arr_opts << repository arr_opts << clone_dir command('clone', arr_opts) return_base_opts_from_clone(clone_dir, opts) end def return_base_opts_from_clone(clone_dir, opts) base_opts = {} base_opts[:repository] = clone_dir if (opts[:bare] || opts[:mirror]) base_opts[:working_directory] = clone_dir unless (opts[:bare] || opts[:mirror]) base_opts[:log] = opts[:log] if opts[:log] base_opts end ## READ COMMANDS ## # # Returns most recent tag that is reachable from a commit # # accepts options: # :all # :tags # :contains # :debug # :exact_match # :dirty # :abbrev # :candidates # :long # :always # :math # # @param [String|NilClass] committish target commit sha or object name # @param [{Symbol=>Object}] opts the given options # @return [String] the tag name # def describe(committish=nil, opts={}) arr_opts = [] arr_opts << '--all' if opts[:all] arr_opts << '--tags' if opts[:tags] arr_opts << '--contains' if opts[:contains] arr_opts << '--debug' if opts[:debug] arr_opts << '--long' if opts[:long] arr_opts << '--always' if opts[:always] arr_opts << '--exact-match' if opts[:exact_match] || opts[:"exact-match"] arr_opts << '--dirty' if opts[:dirty] == true arr_opts << "--dirty=#{opts[:dirty]}" if opts[:dirty].is_a?(String) arr_opts << "--abbrev=#{opts[:abbrev]}" if opts[:abbrev] arr_opts << "--candidates=#{opts[:candidates]}" if opts[:candidates] arr_opts << "--match=#{opts[:match]}" if opts[:match] arr_opts << committish if committish return command('describe', arr_opts) end def log_commits(opts={}) arr_opts = log_common_options(opts) arr_opts << '--pretty=oneline' arr_opts += log_path_options(opts) command_lines('log', arr_opts).map { |l| l.split.first } end def full_log_commits(opts={}) arr_opts = log_common_options(opts) arr_opts << '--pretty=raw' arr_opts << "--skip=#{opts[:skip]}" if opts[:skip] arr_opts += log_path_options(opts) full_log = command_lines('log', arr_opts) process_commit_log_data(full_log) end def revparse(string) return string if string =~ /^[A-Fa-f0-9]{40}$/ # passing in a sha - just no-op it rev = ['head', 'remotes', 'tags'].map do |d| File.join(@git_dir, 'refs', d, string) end.find do |path| File.file?(path) end return File.read(rev).chomp if rev command('rev-parse', string) end def namerev(string) command('name-rev', string).split[1] end def object_type(sha) command('cat-file', '-t', sha) end def object_size(sha) command('cat-file', '-s', sha).to_i end # returns useful array of raw commit object data def commit_data(sha) sha = sha.to_s cdata = command_lines('cat-file', 'commit', sha) process_commit_data(cdata, sha, 0) end def process_commit_data(data, sha = nil, indent = 4) hsh = { 'sha' => sha, 'message' => '', 'parent' => [] } loop do key, *value = data.shift.split break if key.nil? if key == 'parent' hsh['parent'] << value.join(' ') else hsh[key] = value.join(' ') end end hsh['message'] = data.collect {|line| line[indent..-1]}.join("\n") + "\n" return hsh end def tag_data(name) sha = sha.to_s tdata = command_lines('cat-file', 'tag', name) process_tag_data(tdata, name, 0) end def process_tag_data(data, name, indent=4) hsh = { 'name' => name, 'message' => '' } loop do key, *value = data.shift.split break if key.nil? hsh[key] = value.join(' ') end hsh['message'] = data.collect {|line| line[indent..-1]}.join("\n") + "\n" return hsh end def process_commit_log_data(data) in_message = false hsh_array = [] hsh = nil data.each do |line| line = line.chomp if line[0].nil? in_message = !in_message next end in_message = false if in_message && line[0..3] != " " if in_message hsh['message'] << "#{line[4..-1]}\n" next end key, *value = line.split value = value.join(' ') case key when 'commit' hsh_array << hsh if hsh hsh = {'sha' => value, 'message' => '', 'parent' => []} when 'parent' hsh['parent'] << value else hsh[key] = value end end hsh_array << hsh if hsh return hsh_array end def object_contents(sha, &block) command('cat-file', '-p', sha, &block) end def ls_tree(sha) data = {'blob' => {}, 'tree' => {}} command_lines('ls-tree', sha).each do |line| (info, filenm) = line.split("\t") (mode, type, sha) = info.split data[type][filenm] = {:mode => mode, :sha => sha} end data end def mv(file1, file2) command_lines('mv', '--', file1, file2) end def full_tree(sha) command_lines('ls-tree', '-r', sha) end def tree_depth(sha) full_tree(sha).size end def change_head_branch(branch_name) command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}") end def branches_all arr = [] command_lines('branch', '-a').each do |b| current = (b[0, 2] == '* ') arr << [b.gsub('* ', '').strip, current] end arr end def worktrees_all arr = [] directory = '' # Output example for `worktree list --porcelain`: # worktree /code/public/ruby-git # HEAD 4bef5abbba073c77b4d0ccc1ffcd0ed7d48be5d4 # branch refs/heads/master # # worktree /tmp/worktree-1 # HEAD b8c63206f8d10f57892060375a86ae911fad356e # detached # command_lines('worktree',['list', '--porcelain']).each do |w| s = w.split("\s") directory = s[1] if s[0] == 'worktree' arr << [directory, s[1]] if s[0] == 'HEAD' end arr end def worktree_add(dir, commitish = nil) return command('worktree', ['add', dir, commitish]) if !commitish.nil? command('worktree', ['add', dir]) end def worktree_remove(dir) command('worktree', ['remove', dir]) end def worktree_prune command('worktree', ['prune']) end def list_files(ref_dir) dir = File.join(@git_dir, 'refs', ref_dir) files = [] Dir.chdir(dir) { files = Dir.glob('**/*').select { |f| File.file?(f) } } rescue nil files end def branch_current branches_all.select { |b| b[1] }.first[0] rescue nil end def branch_contains(commit, branch_name="") command("branch", [branch_name, "--contains", commit]) end # returns hash # [tree-ish] = [[line_no, match], [line_no, match2]] # [tree-ish] = [[line_no, match], [line_no, match2]] def grep(string, opts = {}) opts[:object] ||= 'HEAD' grep_opts = ['-n'] grep_opts << '-i' if opts[:ignore_case] grep_opts << '-v' if opts[:invert_match] grep_opts << '-e' grep_opts << string grep_opts << opts[:object] if opts[:object].is_a?(String) grep_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String hsh = {} command_lines('grep', grep_opts).each do |line| if m = /(.*)\:(\d+)\:(.*)/.match(line) hsh[m[1]] ||= [] hsh[m[1]] << [m[2].to_i, m[3]] end end hsh end def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) diff_opts = ['-p'] diff_opts << obj1 diff_opts << obj2 if obj2.is_a?(String) diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String command('diff', diff_opts) end def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) diff_opts = ['--numstat'] diff_opts << obj1 diff_opts << obj2 if obj2.is_a?(String) diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String hsh = {:total => {:insertions => 0, :deletions => 0, :lines => 0, :files => 0}, :files => {}} command_lines('diff', diff_opts).each do |file| (insertions, deletions, filename) = file.split("\t") hsh[:total][:insertions] += insertions.to_i hsh[:total][:deletions] += deletions.to_i hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions]) hsh[:total][:files] += 1 hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i} end hsh end def diff_name_status(reference1 = nil, reference2 = nil, opts = {}) opts_arr = ['--name-status'] opts_arr << reference1 if reference1 opts_arr << reference2 if reference2 opts_arr << '--' << opts[:path] if opts[:path] command_lines('diff', opts_arr).inject({}) do |memo, line| status, path = line.split("\t") memo[path] = status memo end end # compares the index and the working directory def diff_files diff_as_hash('diff-files') end # compares the index and the repository def diff_index(treeish) diff_as_hash('diff-index', treeish) end def ls_files(location=nil) location ||= '.' hsh = {} command_lines('ls-files', '--stage', location).each do |line| (info, file) = line.split("\t") (mode, sha, stage) = info.split file = eval(file) if file =~ /^\".*\"$/ # This takes care of quoted strings returned from git hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage} end hsh end def ls_remote(location=nil, opts={}) arr_opts = [] arr_opts << ['--refs'] if opts[:refs] arr_opts << (location || '.') Hash.new{ |h,k| h[k] = {} }.tap do |hsh| command_lines('ls-remote', arr_opts).each do |line| (sha, info) = line.split("\t") (ref, type, name) = info.split('/', 3) type ||= 'head' type = 'branches' if type == 'heads' value = {:ref => ref, :sha => sha} hsh[type].update( name.nil? ? value : { name => value }) end end end def ignored_files command_lines('ls-files', '--others', '-i', '--exclude-standard') end def config_remote(name) hsh = {} config_list.each do |key, value| if /remote.#{name}/.match(key) hsh[key.gsub("remote.#{name}.", '')] = value end end hsh end def config_get(name) do_get = Proc.new do |path| command('config', '--get', name) end if @git_dir Dir.chdir(@git_dir, &do_get) else do_get.call end end def global_config_get(name) command('config', '--global', '--get', name) end def config_list build_list = Proc.new do |path| parse_config_list command_lines('config', '--list') end if @git_dir Dir.chdir(@git_dir, &build_list) else build_list.call end end def global_config_list parse_config_list command_lines('config', '--global', '--list') end def parse_config_list(lines) hsh = {} lines.each do |line| (key, *values) = line.split('=') hsh[key] = values.join('=') end hsh end def parse_config(file) parse_config_list command_lines('config', '--list', '--file', file) end # Shows objects # # @param [String|NilClass] objectish the target object reference (nil == HEAD) # @param [String|NilClass] path the path of the file to be shown # @return [String] the object information def show(objectish=nil, path=nil) arr_opts = [] arr_opts << (path ? "#{objectish}:#{path}" : objectish) command('show', arr_opts.compact, chomp: false) end ## WRITE COMMANDS ## def config_set(name, value, options = {}) if options[:file].to_s.empty? command('config', name, value) else command('config', '--file', options[:file], name, value) end end def global_config_set(name, value) command('config', '--global', name, value) end # updates the repository index using the working directory content # # lib.add('path/to/file') # lib.add(['path/to/file1','path/to/file2']) # lib.add(:all => true) # # options: # :all => true # :force => true # # @param [String,Array] paths files paths to be added to the repository # @param [Hash] options def add(paths='.',options={}) arr_opts = [] arr_opts << '--all' if options[:all] arr_opts << '--force' if options[:force] arr_opts << '--' arr_opts << paths arr_opts.flatten! command('add', arr_opts) end def remove(path = '.', opts = {}) arr_opts = ['-f'] # overrides the up-to-date check by default arr_opts << ['-r'] if opts[:recursive] arr_opts << ['--cached'] if opts[:cached] arr_opts << '--' if path.is_a?(Array) arr_opts += path else arr_opts << path end command('rm', arr_opts) end # Takes the commit message with the options and executes the commit command # # accepts options: # :amend # :all # :allow_empty # :author # :date # :no_verify # :allow_empty_message # :gpg_sign # # @param [String] message the commit message to be used # @param [Hash] opts the commit options to be used def commit(message, opts = {}) arr_opts = [] arr_opts << "--message=#{message}" if message arr_opts << '--amend' << '--no-edit' if opts[:amend] arr_opts << '--all' if opts[:add_all] || opts[:all] arr_opts << '--allow-empty' if opts[:allow_empty] arr_opts << "--author=#{opts[:author]}" if opts[:author] arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String arr_opts << '--no-verify' if opts[:no_verify] arr_opts << '--allow-empty-message' if opts[:allow_empty_message] if opts[:gpg_sign] arr_opts << if opts[:gpg_sign] == true '--gpg-sign' else "--gpg-sign=#{opts[:gpg_sign]}" end end command('commit', arr_opts) end def reset(commit, opts = {}) arr_opts = [] arr_opts << '--hard' if opts[:hard] arr_opts << commit if commit command('reset', arr_opts) end def clean(opts = {}) arr_opts = [] arr_opts << '--force' if opts[:force] arr_opts << '-ff' if opts[:ff] arr_opts << '-d' if opts[:d] arr_opts << '-x' if opts[:x] command('clean', arr_opts) end def revert(commitish, opts = {}) # Forcing --no-edit as default since it's not an interactive session. opts = {:no_edit => true}.merge(opts) arr_opts = [] arr_opts << '--no-edit' if opts[:no_edit] arr_opts << commitish command('revert', arr_opts) end def apply(patch_file) arr_opts = [] arr_opts << '--' << patch_file if patch_file command('apply', arr_opts) end def apply_mail(patch_file) arr_opts = [] arr_opts << '--' << patch_file if patch_file command('am', arr_opts) end def stashes_all arr = [] filename = File.join(@git_dir, 'logs/refs/stash') if File.exist?(filename) File.open(filename) do |f| f.each_with_index do |line, i| m = line.match(/:(.*)$/) arr << [i, m[1].strip] end end end arr end def stash_save(message) output = command('stash save', message) output =~ /HEAD is now at/ end def stash_apply(id = nil) if id command('stash apply', id) else command('stash apply') end end def stash_clear command('stash clear') end def stash_list command('stash list') end def branch_new(branch) command('branch', branch) end def branch_delete(branch) command('branch', '-D', branch) end def checkout(branch, opts = {}) arr_opts = [] arr_opts << '-b' if opts[:new_branch] || opts[:b] arr_opts << '--force' if opts[:force] || opts[:f] arr_opts << branch command('checkout', arr_opts) end def checkout_file(version, file) arr_opts = [] arr_opts << version arr_opts << file command('checkout', arr_opts) end def merge(branch, message = nil, opts = {}) arr_opts = [] arr_opts << '--no-commit' if opts[:no_commit] arr_opts << '--no-ff' if opts[:no_ff] arr_opts << '-m' << message if message arr_opts += [branch] command('merge', arr_opts) end def merge_base(*args) opts = args.last.is_a?(Hash) ? args.pop : {} arg_opts = [] arg_opts << '--octopus' if opts[:octopus] arg_opts << '--independent' if opts[:independent] arg_opts << '--fork-point' if opts[:fork_point] arg_opts << '--all' if opts[:all] arg_opts += args command('merge-base', arg_opts).lines.map(&:strip) end def unmerged unmerged = [] command_lines('diff', "--cached").each do |line| unmerged << $1 if line =~ /^\* Unmerged path (.*)/ end unmerged end def conflicts # :yields: file, your, their self.unmerged.each do |f| your_tempfile = Tempfile.new("YOUR-#{File.basename(f)}") your = your_tempfile.path your_tempfile.close # free up file for git command process command('show', ":2:#{f}", redirect: "> #{escape your}") their_tempfile = Tempfile.new("THEIR-#{File.basename(f)}") their = their_tempfile.path their_tempfile.close # free up file for git command process command('show', ":3:#{f}", redirect: "> #{escape their}") yield(f, your, their) end end def remote_add(name, url, opts = {}) arr_opts = ['add'] arr_opts << '-f' if opts[:with_fetch] || opts[:fetch] arr_opts << '-t' << opts[:track] if opts[:track] arr_opts << '--' arr_opts << name arr_opts << url command('remote', arr_opts) end def remote_set_url(name, url) arr_opts = ['set-url'] arr_opts << name arr_opts << url command('remote', arr_opts) end def remote_remove(name) command('remote', 'rm', name) end def remotes command_lines('remote') end def tags command_lines('tag') end def tag(name, *opts) target = opts[0].instance_of?(String) ? opts[0] : nil opts = opts.last.instance_of?(Hash) ? opts.last : {} if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message]) raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]." end arr_opts = [] arr_opts << '-f' if opts[:force] || opts[:f] arr_opts << '-a' if opts[:a] || opts[:annotate] arr_opts << '-s' if opts[:s] || opts[:sign] arr_opts << '-d' if opts[:d] || opts[:delete] arr_opts << name arr_opts << target if target if opts[:m] || opts[:message] arr_opts << '-m' << (opts[:m] || opts[:message]) end command('tag', arr_opts) end def fetch(remote, opts) arr_opts = [remote] arr_opts << opts[:ref] if opts[:ref] arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] arr_opts << '--unshallow' if opts[:unshallow] arr_opts << '--depth' << opts[:depth] if opts[:depth] command('fetch', arr_opts) end def push(remote, branch = 'master', opts = {}) # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature. opts = {:tags => opts} if [true, false].include?(opts) arr_opts = [] arr_opts << '--mirror' if opts[:mirror] arr_opts << '--delete' if opts[:delete] arr_opts << '--force' if opts[:force] || opts[:f] arr_opts << remote if opts[:mirror] command('push', arr_opts) else command('push', arr_opts + [branch]) command('push', ['--tags'] + arr_opts) if opts[:tags] end end def pull(remote='origin', branch='master') command('pull', remote, branch) end def tag_sha(tag_name) head = File.join(@git_dir, 'refs', 'tags', tag_name) return File.read(head).chomp if File.exist?(head) command('show-ref', '--tags', '-s', tag_name) end def repack command('repack', '-a', '-d') end def gc command('gc', '--prune', '--aggressive', '--auto') end # reads a tree into the current index file def read_tree(treeish, opts = {}) arr_opts = [] arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix] arr_opts += [treeish] command('read-tree', arr_opts) end def write_tree command('write-tree') end def commit_tree(tree, opts = {}) opts[:message] ||= "commit tree #{tree}" t = Tempfile.new('commit-message') t.write(opts[:message]) t.close arr_opts = [] arr_opts << tree arr_opts << '-p' << opts[:parent] if opts[:parent] arr_opts += [opts[:parents]].map { |p| ['-p', p] }.flatten if opts[:parents] command('commit-tree', arr_opts, redirect: "< #{escape t.path}") end def update_ref(branch, commit) command('update-ref', branch, commit) end def checkout_index(opts = {}) arr_opts = [] arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix] arr_opts << "--force" if opts[:force] arr_opts << "--all" if opts[:all] arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String command('checkout-index', arr_opts) end # creates an archive file # # options # :format (zip, tar) # :prefix # :remote # :path def archive(sha, file = nil, opts = {}) opts[:format] ||= 'zip' if opts[:format] == 'tgz' opts[:format] = 'tar' opts[:add_gzip] = true end if !file tempfile = Tempfile.new('archive') file = tempfile.path # delete it now, before we write to it, so that Ruby doesn't delete it # when it finalizes the Tempfile. tempfile.close! end arr_opts = [] arr_opts << "--format=#{opts[:format]}" if opts[:format] arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix] arr_opts << "--remote=#{opts[:remote]}" if opts[:remote] arr_opts << sha arr_opts << '--' << opts[:path] if opts[:path] command('archive', arr_opts, redirect: " > #{escape file}") if opts[:add_gzip] file_content = File.read(file) Zlib::GzipWriter.open(file) do |gz| gz.write(file_content) end end return file end # returns the current version of git, as an Array of Fixnums. def current_command_version output = command('version') version = output[/\d+\.\d+(\.\d+)+/] version.split('.').collect {|i| i.to_i} end def required_command_version [1, 6] end def meets_required_version? (self.current_command_version <=> self.required_command_version) >= 0 end private # Systen ENV variables involved in the git commands. # # @return [] the names of the EVN variables involved in the git commands ENV_VARIABLE_NAMES = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_SSH'] def command_lines(cmd, *opts) cmd_op = command(cmd, *opts) if cmd_op.encoding.name != "UTF-8" op = cmd_op.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace) else op = cmd_op end op.split("\n") end # Takes the current git's system ENV variables and store them. def store_git_system_env_variables @git_system_env_variables = {} ENV_VARIABLE_NAMES.each do |env_variable_name| @git_system_env_variables[env_variable_name] = ENV[env_variable_name] end end # Takes the previously stored git's ENV variables and set them again on ENV. def restore_git_system_env_variables ENV_VARIABLE_NAMES.each do |env_variable_name| ENV[env_variable_name] = @git_system_env_variables[env_variable_name] end end # Sets git's ENV variables to the custom values for the current instance. def set_custom_git_env_variables ENV['GIT_DIR'] = @git_dir ENV['GIT_WORK_TREE'] = @git_work_dir ENV['GIT_INDEX_FILE'] = @git_index_file ENV['GIT_SSH'] = Git::Base.config.git_ssh end # Runs a block inside an environment with customized ENV variables. # It restores the ENV after execution. # # @param [Proc] block block to be executed within the customized environment def with_custom_env_variables(&block) @@semaphore.synchronize do store_git_system_env_variables() set_custom_git_env_variables() return block.call() end ensure restore_git_system_env_variables() end def command(cmd, *opts, &block) command_opts = { chomp: true, redirect: '' } if opts.last.is_a?(Hash) command_opts.merge!(opts.pop) end command_opts.keys.each do |k| raise ArgumentError.new("Unsupported option: #{k}") unless [:chomp, :redirect].include?(k) end global_opts = [] global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil? global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? global_opts << ["-c", "color.ui=false"] opts = [opts].flatten.map {|s| escape(s) }.join(' ') global_opts = global_opts.flatten.map {|s| escape(s) }.join(' ') git_cmd = "#{Git::Base.config.binary_path} #{global_opts} #{cmd} #{opts} #{command_opts[:redirect]} 2>&1" output = nil command_thread = nil; exitstatus = nil with_custom_env_variables do command_thread = Thread.new do output = run_command(git_cmd, &block) exitstatus = $?.exitstatus end command_thread.join end if @logger @logger.info(git_cmd) @logger.debug(output) end raise Git::GitExecuteError, "#{git_cmd}:#{output}" if exitstatus > 1 || (exitstatus == 1 && output != '') output.chomp! if output && command_opts[:chomp] && !block_given? output end # Takes the diff command line output (as Array) and parse it into a Hash # # @param [String] diff_command the diff commadn to be used # @param [Array] opts the diff options to be used # @return [Hash] the diff as Hash def diff_as_hash(diff_command, opts=[]) # update index before diffing to avoid spurious diffs command('status') command_lines(diff_command, opts).inject({}) do |memo, line| info, file = line.split("\t") mode_src, mode_dest, sha_src, sha_dest, type = info.split memo[file] = { :mode_index => mode_dest, :mode_repo => mode_src.to_s[1, 7], :path => file, :sha_repo => sha_src, :sha_index => sha_dest, :type => type } memo end end # Returns an array holding the common options for the log commands # # @param [Hash] opts the given options # @return [Array] the set of common options that the log command will use def log_common_options(opts) arr_opts = [] arr_opts << "-#{opts[:count]}" if opts[:count] arr_opts << "--no-color" arr_opts << "--cherry" if opts[:cherry] arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2) arr_opts end # Retrurns an array holding path options for the log commands # # @param [Hash] opts the given options # @return [Array] the set of path options that the log command will use def log_path_options(opts) arr_opts = [] arr_opts << opts[:object] if opts[:object].is_a? String arr_opts << '--' << opts[:path_limiter] if opts[:path_limiter] arr_opts end def default_encoding __ENCODING__.name end def best_guess_encoding # Encoding::ASCII_8BIT.name Encoding::UTF_8.name end def detected_encoding(str) CharDet.detect(str)['encoding'] || best_guess_encoding end def encoding_options { invalid: :replace, undef: :replace } end def normalize_encoding(str) return str if str.valid_encoding? && str.encoding.name == default_encoding return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding? str.encode(default_encoding, detected_encoding(str), **encoding_options) end def run_command(git_cmd, &block) return IO.popen(git_cmd, &block) if block_given? `#{git_cmd}`.lines.map { |l| normalize_encoding(l) }.join end def escape(s) windows_platform? ? escape_for_windows(s) : escape_for_sh(s) end def escape_for_sh(s) "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'" end def escape_for_windows(s) # Windows does not need single quote escaping inside double quotes %Q{"#{s}"} end def windows_platform? # Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby) win_platform_regex = /mingw|mswin/ RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex end end end