class GitConflictBlame

Git command that shows the blame on the lines that are in conflict. This should be ran after a “git merge” command has been ran and there are files that are in conflict.

Constants

VERSION

Public Class Methods

new( json: false, pretty_json: false ) click to toggle source

@param json [Boolean] Whether or not to output in JSON format @param pretty_json [Boolean] Whether or not to output in pretty JSON format

# File lib/git-conflict-blame.rb, line 13
def initialize( json: false, pretty_json: false )
  @json        = json
  @pretty_json = pretty_json
  @current_dir = Dir.pwd
  @repo = Rugged::Repository.discover( @current_dir )
end
run( options ) click to toggle source

Main public method to run on the class

@param options [Hash] Run options

# File lib/git-conflict-blame.rb, line 23
def self.run( options )
  new( options ).run!
end

Public Instance Methods

run!() click to toggle source

Actually performs the conflict blame

# File lib/git-conflict-blame.rb, line 28
def run!
  if conflicts?
    Dir.chdir( @repo.workdir )
    @submodules = find_submodules
    log "#{conflict_count} files are in conflict".red
    log "Parsing files to find out who is to blame..."
    data, total_conflicts = find_conflict_blames

    if total_conflicts == 0
      if conflict_count == 0
        log "All conflicts appear to be resolved".green
      else
        log "\nThere are #{conflict_count} files in conflict that cannot be blamed:".red
        remaining_conflicted_files = cmd( 'git diff --name-only --diff-filter=U' )
        log remaining_conflicted_files
        if @json
          data = {}
          remaining_conflicted_files.split( "\n" ).each do |file_name|
            data[file_name] = 'conflict cannot be blamed'
          end
        end
      end
    else
      log "#{total_conflicts} total conflicts found!\n".red
    end

    if @json
      json_data = {
        exception:   false,
        file_count:  conflict_count,
        total_count: total_conflicts,
        data:        data
      }
      output_json( json_data )
    else
      display_results( data )
    end
  else
    log 'No conflicts found'.green
  end
rescue GitError => e
  log_error( e )
  exit 1
rescue CmdError => e
  message =  "This is probably a bug. Please log it here:\n"
  message << "https://github.com/eterry1388/git-conflict-blame/issues"
  log_error( e, message: message )
  exit 1
rescue Exception => e
  message =  "#{e.backtrace.join( "\n" )}\n"
  message << "Unhandled exception! This is probably a bug. Please log it here:\n"
  message << "https://github.com/eterry1388/git-conflict-blame/issues"
  log_error( e, message: message )
  exit 1
ensure
  Dir.chdir( @current_dir )
end

Private Instance Methods

cmd( command ) click to toggle source

Run a command

@param command [String] the command to run @raise [CmdError] if command is not successful @return [String] the output (stdout) of running the command

# File lib/git-conflict-blame.rb, line 121
def cmd( command )
  stdout, stderr, status = Open3.capture3( command )
  unless status.success?
    raise CmdError, "Command: '#{command}', Stdout: '#{stdout}', Stderr: '#{stderr}'"
  end
  stdout
rescue Errno::ENOENT => e
  raise CmdError, "Command: '#{command}', Error: '#{e}'"
end
conflict_count() click to toggle source

Gets the total number of files that are in conflict

@return [Integer] Number of files in conflict

# File lib/git-conflict-blame.rb, line 151
def conflict_count
  @conflict_count ||= @repo.index.conflicts.count
end
conflicts() click to toggle source

Gets all the filenames that are in conflict

@return [Array<String>]

# File lib/git-conflict-blame.rb, line 141
def conflicts
  @conflicts ||= @repo.index.conflicts.map do |conflict|
    ours = conflict[:ours]
    ours[:path] if ours
  end.compact
end
conflicts?() click to toggle source

Figures out if there are any conflicts in the git repo

@return [Boolean]

# File lib/git-conflict-blame.rb, line 134
def conflicts?
  @repo.index.conflicts?
end
display_results( data ) click to toggle source

Outputs the results of {#find_conflict_blames} to the console

@param data [Hash]

# File lib/git-conflict-blame.rb, line 262
def display_results( data )
  data.each do |filename, conflicts|
    log filename.green
    conflicts.each do |lines|
      lines.each do |line|
        git_info = [line[:commit_id], line[:email][0..30], line[:date]]
        formatted_git_info =  "\t%-10s %-30s %-13s" % git_info

        if line[:line_content].include?( '<<<<<<<' ) || line[:line_content].include?( '>>>>>>>' )
          line_color = :red
        elsif line[:line_content].include?( '=======' )
          line_color = :yellow
        else
          line_color = :light_blue
        end

        line_data = [formatted_git_info, line[:line_number].to_s.bold, line[:line_content].colorize( line_color )]
        log "%s [%-20s]  %-s" % line_data
      end
      log # New line
    end
  end
end
find_conflict_blames() click to toggle source

Parses through the conflicted files and finds the blame on each line

@return [Array] [data_hash, conflict_count]

# File lib/git-conflict-blame.rb, line 180
def find_conflict_blames
  data = {}
  total_conflicts = 0

  conflicts.each do |conflict|
    begin
      next if @submodules.include?( conflict )
      raw = raw_blame( conflict )
      start_indexes = []
      end_indexes = []
      lines = raw.split( "\n" )
      lines.each do |line|
        start_indexes << lines.index( line ) if line.include?( '<<<<<<<' )
        end_indexes   << lines.index( line ) if line.include?( '>>>>>>>' )
      end

      index = 0
      all_line_sets = []
      start_indexes.count.times do
        start_index = start_indexes[index]
        end_index = end_indexes[index]
        line_set = lines[start_index..end_index]
        all_line_sets << parse_lines( line_set )
        index += 1
      end
      total_conflicts += start_indexes.count
      data[conflict] = all_line_sets
    rescue ArgumentError
      # Do nothing
      # Caused by encoding issues like "invalid byte sequence in UTF-8"
    end
  end
  data.delete_if { |_, all_line_sets| all_line_sets.nil? || all_line_sets.empty? }

  [data, total_conflicts]
end
find_submodules() click to toggle source

Find all submodules in git repo

@return [Array] List of submodule paths

# File lib/git-conflict-blame.rb, line 166
def find_submodules
  submodules = []
  current_dir = Dir.pwd
  Dir.chdir( @repo.workdir )
  if File.exists?( '.gitmodules' )
    submodules = cmd( "grep path .gitmodules | sed 's/.*= //'" ).split( "\n" )
  end
  Dir.chdir( current_dir )
  submodules
end
log( message = '' ) click to toggle source

Outputs a message

@note This will not output anything if @json is set! @param message [String]

# File lib/git-conflict-blame.rb, line 92
def log( message = '' )
  return if @json
  puts message
end
log_error( e, message: '' ) click to toggle source

Outputs an error message

@param e [Exception] @param message [String] Error message

# File lib/git-conflict-blame.rb, line 101
def log_error( e, message: '' )
  if @json
    message = "#{e.message}\n#{message}" 
    error_hash = {
      exception: true,
      type:      e.class,
      message:   message
    }
    output_json( error_hash )
  else
    puts "#{e.class}: #{e.message}"
    puts message if message
  end
end
output_json( hash ) click to toggle source

Outputs a Hash in JSON format

@param hash [Hash]

# File lib/git-conflict-blame.rb, line 251
def output_json( hash )
  if @pretty_json
    puts JSON.pretty_generate( hash )
  else
    puts hash.to_json
  end
end
parse_line( line ) click to toggle source

Parses a line from the “git blame” command

@param line [String] @return [Hash]

# File lib/git-conflict-blame.rb, line 229
def parse_line( line )
  line_array = line.split( "\t" )
  commit_id = line_array[0]
  email     = line_array[1].delete( '(' ).delete( '<' ).delete( '>' )
  date      = line_array[2]
  raw_line  = line_array[3..999].join( "\t" )
  raw_line_array = raw_line.split( ')' )
  line_number  = raw_line_array[0].to_i
  line_content = raw_line_array[1..999].join( ')' )

  {
    commit_id:    commit_id,
    email:        email,
    date:         date,
    line_number:  line_number,
    line_content: line_content
  }
end
parse_lines( lines ) click to toggle source

Iterates through a line set and parses them

@param lines [Array<String>] @return [Array<Hash>]

# File lib/git-conflict-blame.rb, line 221
def parse_lines( lines )
  lines.map { |line| parse_line( line ) }
end
raw_blame( filename ) click to toggle source

Runs the “git blame” command

@param filename [String] @return [String] Raw results of the “git blame” command

# File lib/git-conflict-blame.rb, line 159
def raw_blame( filename )
  cmd( "git blame --show-email -c --date short #{filename}" )
end