app/models/repository/git_remote.rb
require 'redmine/scm/adapters/git_adapter'
require 'pathname'
require 'fileutils'
# require 'open3'
require_dependency 'redmine_git_remote/poor_mans_capture3'

class Repository::GitRemote < Repository::Git

  before_validation :initialize_clone

  safe_attributes 'extra_info', :if => lambda {|repository, _user| repository.new_record?}

  # TODO: figure out how to do this safely (if at all)
  # before_deletion :rm_removed_repo
  # def rm_removed_repo
  #   if Repository.find_all_by_url(repo.url).length <= 1
  #     system "rm -Rf #{self.clone_path}"
  #   end
  # end

  def extra_clone_url
    return nil unless extra_info
    extra_info["extra_clone_url"]
  end

  def clone_url
    self.extra_clone_url
  end

  def clone_path
    self.url
  end

  def clone_host
    p = parse(clone_url)
    return p[:host]
  end

  def clone_protocol_ssh?
    # Possible valid values (via http://git-scm.com/book/ch4-1.html):
    #  ssh://user@server/project.git
    #  user@server:project.git
    #  server:project.git
    # For simplicity we just assume if it's not HTTP(S), then it's SSH.
    !clone_url.match(/^http/)
  end

  # Hook into Repository.fetch_changesets to also run 'git fetch'.
  def fetch_changesets
    # ensure we don't fetch twice during the same request
    return if @already_fetched
    @already_fetched = true

    puts "Calling fetch changesets on #{clone_path}"
    # runs git fetch
    self.fetch
    super
  end

  # Override default_branch to fetch, otherwise caching problems in
  # find_project_repository prevent Repository::Git#fetch_changesets from running.
  #
  # Ideally this would only be run for RepositoriesController#show.
  def default_branch
    if self.branches == [] && self.project.active? && Setting.autofetch_changesets?
      # git_adapter#branches caches @branches incorrectly, reset it
      scm.instance_variable_set :@branches, nil
      # NB: fetch_changesets is idemptotent during a given request, so OK to call it 2x
      self.fetch_changesets
    end
    super
  end

  # called in before_validate handler, sets form errors
  def initialize_clone
    # avoids crash in RepositoriesController#destroy
    return unless attributes["extra_info"]["extra_clone_url"]

    p = parse(attributes["extra_info"]["extra_clone_url"])
    self.identifier = p[:identifier] if identifier.empty?

    base_path = Setting.plugin_redmine_git_remote['git_remote_repo_clone_path']
    base_path = base_path + "/" unless base_path.end_with?("/")

    self.url = base_path + p[:path] if url.empty?

    err = ensure_possibly_empty_clone_exists
    errors.add :extra_clone_url, err if err
  end

  # equality check ignoring trailing whitespace and slashes
  def two_remotes_equal(a,b)
    a.chomp.gsub(/\/$/,'') == b.chomp.gsub(/\/$/,'')
  end

  def ensure_possibly_empty_clone_exists
    Repository::GitRemote.add_known_host(clone_host) if clone_protocol_ssh?

    unless system "git", "ls-remote",  "-h",  clone_url
      return "#{clone_url} is not a valid remote."
    end

    if Dir.exists? clone_path
      existing_repo_remote, status = RedmineGitRemote::PoorMansCapture3::capture2("git", "--git-dir", clone_path, "config", "--get", "remote.origin.url")
      return "Unable to run: git --git-dir #{clone_path} config --get remote.origin.url" unless status.success?

      unless two_remotes_equal(existing_repo_remote, clone_url)
        return "Directory '#{clone_path}' already exits, unmatching clone url: #{existing_repo_remote}"
      end
    else
      unless system "git", "init", "--bare", clone_path
        return  "Unable to run: git init --bare #{clone_path}"
      end

      unless system "git", "--git-dir", clone_path, "remote", "add", "--mirror=fetch", "origin",  clone_url
        return  "Unable to run: git --git-dir #{clone_path} remote add --mirror=fetch origin #{clone_url}"
      end
    end
  end

  unloadable
  def self.scm_name
    'GitRemote'
  end

  # TODO: first validate git URL and display error message
  def parse(url)
    url.strip!

    ret = {}
    # start with http://github.com/evolvingweb/git_remote or git@git.ewdev.ca:some/repo.git
    ret[:url] = url

    # NB: Starting lines with ".gsub" is a syntax error in ruby 1.8.
    #     See http://stackoverflow.com/q/12906048/9621
    # path is github.com/evolvingweb/muhc-ci
    ret[:path] = url.gsub(/^.*:\/\//, '').   # Remove anything before ://
                     gsub(/:/, '/').         # convert ":" to "/"
                     gsub(/^.*@/, '').       # Remove anything before @
                     gsub(/\.git$/, '')      # Remove trailing .git
    ret[:host] = ret[:path].split('/').first
    #TODO: handle project uniqueness automatically or prompt
    ret[:identifier] =   ret[:path].split('/').last.downcase.gsub(/[^a-z0-9_-]/,'-')
    return ret
  end

  def fetch
    puts "Fetching repo #{clone_path}"
    Repository::GitRemote.add_known_host(clone_host) if clone_protocol_ssh?

    err = ensure_possibly_empty_clone_exists
    Rails.logger.warn err if err

    # If dir exists and non-empty, should be safe to 'git fetch'
    unless system "git", "--git-dir", clone_path, "fetch", "--all"
      Rails.logger.warn "Unable to run 'git -c #{clone_path} fetch --all'"
    end
  end

  # Checks if host is in ~/.ssh/known_hosts, adds it if not present
  def self.add_known_host(host)
    # if not found...
    out, status = RedmineGitRemote::PoorMansCapture3::capture2("ssh-keygen", "-F", host)
    raise "Unable to run 'ssh-keygen -F #{host}" unless status
    unless out.match /found/
      # hack to work with 'docker exec' where HOME isn't set (or set to /)
      ssh_dir = (ENV['HOME'] == "/" || ENV['HOME'] == nil ? "/root" : ENV['HOME']) + "/.ssh"
      ssh_known_hosts = ssh_dir + "/known_hosts"
      begin
        FileUtils.mkdir_p ssh_dir
      rescue Exception => e
        raise "Unable to create directory #{ssh_dir}: " + e.to_s
      end

      puts "Adding #{host} to #{ssh_known_hosts}"
      out, status = RedmineGitRemote::PoorMansCapture3::capture2("ssh-keyscan", host)
      raise "Unable to run 'ssh-keyscan #{host}'" unless status
      Kernel::open(ssh_known_hosts, 'a') { |f| f.puts out}
    end
  end
end
