fortinmike/git-multirepo

View on GitHub
lib/multirepo/commands/install-command.rb

Summary

Maintainability
A
50 mins
Test Coverage
require "terminal-table"

require "multirepo/utility/console"
require "multirepo/utility/utils"
require "multirepo/output/extra-output"
require "multirepo/git/repo"
require "multirepo/logic/performer"
require "multirepo/commands/checkout-command"

module MultiRepo
  class InstallCommand < Command
    self.command = "install"
    self.summary = "Clones and checks out dependencies as defined in the version-controlled multirepo metadata files and installs git-multirepo's local git hooks."
    
    def self.options
      [
        ['[--hooks]', 'Only install local git hooks.'],
        ['[--ci]', 'Perform a continuous-integration-aware install (such as on a CI build server or agent).']
      ].concat(super)
    end
    
    def initialize(argv)
      @hooks = argv.flag?("hooks")
      @ci = argv.flag?("ci")
      super
    end
    
    def validate!
      super
      unless Utils.only_one_true?(@hooks, @ci)
        help! "You can't provide more than one operation modifier (--hooks, --ci, etc.)"
      end
    end
        
    def run
      ensure_in_work_tree unless @ci
      ensure_multirepo_tracked
      
      if @hooks
        Console.log_step("Installing hooks in main repo and all dependencies...")
        install_hooks_step
      else
        Console.log_step("Installing dependencies...")
        log_ci_info if @ci
        full_install
      end
      
      Console.log_step("Done!")
    end
    
    def full_install
      install_dependencies_step
      install_hooks_step unless @ci
      update_gitconfigs_step unless @ci
    end
    
    def install_dependencies_step
      # Read config entries as-is on disk, without prior checkout
      config_entries = ConfigFile.new(".").load_entries
      Console.log_substep("Installing #{config_entries.count} dependencies...")
      
      # Clone or fetch all configured dependencies to make sure nothing is missing locally
      Performer.depth_ordered_dependencies.each { |d| clone_or_fetch(d) }
      
      # Checkout the appropriate branches as specified in the lock file
      ExtraOutput.progress("Checking out appropriate dependency revisions") if @ci
      checkout_command = CheckoutCommand.new(CLAide::ARGV.new([]))
      mode = @ci ? RevisionSelection::AS_LOCK : RevisionSelection::LATEST
      checkout_command.dependencies_checkout_step(mode, nil, @ci) # Force in CI environment
    end
    
    def install_hooks_step
      perform_in_main_repo_and_dependencies("Installed git hooks") { |repo| install_hooks(repo) }
    end
    
    def update_gitconfigs_step
      perform_in_main_repo_and_dependencies("Updated .git/config file") { |repo| update_gitconfig(repo) }
    end
    
    def perform_in_main_repo_and_dependencies(message_prefix, &operation)
      operation.call(".")
      Console.log_substep("#{message_prefix} in main repo")
      
      multirepo_enabled_dependencies.each do |config_entry|
        operation.call(config_entry.repo.path)
        Console.log_substep("#{message_prefix} in multirepo-enabled dependency '#{config_entry.repo.path}'")
      end
    end
    
    # Repo operations
    
    def clone_or_fetch(dependency)
      if dependency.config_entry.repo.exists?
        check_repo_validity(dependency)
        
        Console.log_substep("Working copy '#{dependency.config_entry.repo.path}' already exists, fetching...") 
        ExtraOutput.progress("Fetching #{dependency.config_entry.repo.basename}") if @ci
        fetch_repo(dependency)
      else
        Console.log_substep("Cloning #{dependency.config_entry.url} into '#{dependency.config_entry.repo.path}'")
        ExtraOutput.progress("Cloning into #{dependency.config_entry.repo.basename}") if @ci
        clone_repo(dependency)
      end
    end
    
    def fetch_repo(dependency)
      unless dependency.config_entry.repo.fetch
        ExtraOutput.error("Failed to fetch #{dependency.config_entry.repo.basename}") if @ci
        fail MultiRepoException, "Could not fetch from remote #{dependency.config_entry.repo.remote('origin').url}"
      end
    end
    
    def clone_repo(dependency)
      options = { :branch => dependency.lock_entry.branch, :quiet => @ci }
      unless dependency.config_entry.repo.clone(dependency.config_entry.url, options)
        ExtraOutput.error("Failed to clone #{dependency.config_entry.repo.basename}") if @ci
        fail MultiRepoException, "Could not clone remote #{dependency.config_entry.url} with branch #{dependency.lock_entry.branch}"
      end
    end
    
    # Validation
    
    def check_repo_validity(dependency)
      unless dependency.config_entry.repo.remote("origin").url == dependency.config_entry.url
        ExtraOutput.error("Repo #{dependency.config_entry.path} origin URL does not match config") if @ci
        fail MultiRepoException, "'#{dependency.config_entry.path}' origin URL (#{dependency.config_entry.repo.remote('origin').url}) does not match entry (#{dependency.config_entry.url})!"
      end
    end
    
    # Logging
    
    def log_ci_info
      Console.log_info("Performing continuous-integration-aware install")
      Console.log_info("Using git-multirepo #{MultiRepo::VERSION}")
      
      main_repo = Repo.new(".")
      
      log_merge_commit_warning(main_repo)
      log_merge_table(main_repo)
    end
    
    def log_merge_commit_warning(main_repo)
      if main_repo.head.merge_commit?
        Console.log_warning("[MERGE COMMIT] The checked-out main repo revision is a merge commit.")
        Console.log_warning("[MERGE COMMIT] Lock file might not represent a valid project state.")
      end
    end
    
    def log_merge_table(main_repo)
      meta_file = MetaFile.new(".").load
      main_repo_branch = main_repo.current_branch
      
      puts Terminal::Table.new do |t|
        t.title = "Revision Info"
        t.add_row ["Tracked Using", "git-multirepo #{meta_file.version}"]
        t.add_separator
        t.add_row ["Main Repo", commit_info(main_repo.head.commit_id, (main_repo_branch.name rescue nil))]
        t.add_separator
        LockFile.new(".").load_entries.each do |lock_entry|
          branch_name = lock_entry.branch
          t.add_row [lock_entry.name, commit_info(lock_entry.head, branch_name)]
        end
      end
    end

    def commit_info(commit_id, branch_name)
      commit_id + (branch_name ? " (on branch #{branch_name})" : "")
    end
  end
end