NullVoxPopuli/kdiff3-rb

View on GitHub
lib/kdiff3.rb

Summary

Maintainability
A
1 hr
Test Coverage
# string.present?
require 'active_support/core_ext/string'

module KDiff3

  # list of created Tempfiles
  TEMPFILES = []
  # so we don't accidentally mess up formatting when we remove these later,
  # we need to have a weird, non-stand sequence of characters to search
  # and replace
  NEWLINE = "\n\-"


  # performs a 3 way merge between base, a, and b
  # a 2 way merge will be performed if either yours
  # or theirs are left out
  #
  # @param [String] base string or file path for common ancestor
  # @param [String] yours string or file path for your changes
  # @param [String] theirs string or file path for their changes
  # @param [Boolean] html whether or not use an HTML diffing technique
  # @return [String] result of merge
  def self.merge(base: nil, yours: nil, theirs: nil, html: false)
    raise ArgumentError.new('base is required') unless base.present?
    raise ArgumentError.new('yours and/or theirs required') unless yours || theirs

    # since HTML is often compressed to conserve transfer space, and therefore
    # has few lines, we need to split up the HTML in to a multi-lined document
    if html
      base = add_new_lines(base)
      yours = add_new_lines(yours)
      theirs = add_new_lines(theirs)
    end

    base_path = tempfile(text: base, name: 'base')
    your_path = tempfile(text: yours, name: 'yours')
    their_path = tempfile(text: theirs, name: 'theirs')
    output_path = tempfile(name: 'output')

    # we don't need these open for anything
    close_tempfiles

    # the heavy lifting, courtesy of kdiff3
    exit_code = run("#{base_path} #{your_path} #{their_path} -m --auto --fail -o #{output_path}")
    conflicts_exist = exit_code == 1

    result = IO.read(output_path) unless conflicts_exist

    # clean up
    delete_tempfiles

    raise RuntimeError.new("Conflicts exist and could not be resolved") if conflicts_exist

    if html
      # remove the NEWLINES
      result.gsub!(NEWLINE, "")
    end

    result
  end

  private

  # @param [String] name of the file
  # @param [String] text content of file or path
  # @return [String] path of the Tempfile or Pre-existing file
  def self.tempfile(text: nil, name: nil)
    result = ""

    # if the file already exists,
    # don't add it to the tempfile list,
    # as we don't want it to be deleted
    if text && is_file_path?(text)
      result = text
    else
      t = Tempfile.new(name)
      t << text if text
      TEMPFILES << t
      result = t.path
    end

    result
  end

  def self.close_tempfiles
    TEMPFILES.map(&:close)
  end

  def self.delete_tempfiles
    TEMPFILES.map(&:delete)
  end

  # @param [String] path can be a file path or arbitrary string
  # @return [Boolean] if the given string is a path to a file
  def self.is_file_path?(path)
    File.exist?(path)
  end

  def self.run(args)
    %x(
      #{kdiff3_path} #{args}
    )
    $?.exitstatus
  end

  # ensures the local copy of kdiff3 is present, if not, download and compile it
  def self.kdiff3_path
    current_folder = File.dirname(__FILE__)
    path = "#{current_folder}/../ext/kdiff3/releaseQt/kdiff3"

    # update path for mac
    if !RUBY_PLATFORM.match("darwin").nil?
      path = "#{current_folder}/../ext/kdiff3/releaseQt/kdiff3.app/Contents/MacOS/kdiff3"
    end

    unless File.exist?(path)
      raise 'kdiff3 from NullVoxPopuli/kdiff3 was not sucessfully compiled.'
    end

    path
  end

  # add newlines after every tag, and every character
  def self.add_new_lines(text)
    text = self.add_new_lines_to_non_tags(text)
    text = self.add_new_lines_after_tags(text)

    # trim accidental blank lines
    text.gsub!("#{NEWLINE}#{NEWLINE}", NEWLINE)

    text
  end

  # http://www.rubular.com/r/N2AHZgpPum
  # http://stackoverflow.com/questions/7540489/javascript-regex-match-text-not-part-of-a-html-tag
  def self.add_new_lines_after_tags(text)
    tag_selection_regex = /<[^>]*>/
    text.gsub(tag_selection_regex) do |match|
      match << NEWLINE
    end
  end

  # http://www.rubular.com/r/mpX6Ee2r0k
  # http://stackoverflow.com/questions/18621568/regex-replace-text-outside-html-tags
  def self.add_new_lines_to_non_tags(text)
    non_tags = /(?<=^|>)[^><]+?(?=<|$)/
    # non_tags = /([^<>]+)(?![^<]*>|[^<>]*<\/)/
    text.gsub(non_tags) do |match|
      # consecutive words
      match.gsub!(" ", " #{NEWLINE}")
      # end with NEWLINE
      match << NEWLINE
    end
  end


end