require 'update_repo/version' require 'update_repo/helpers' require 'update_repo/cmd_config' require 'yaml' require 'colorize' require 'confoog' require 'trollop' require 'open3' # Overall module with classes performing the functionality # Contains Class UpdateRepo::WalkRepo module UpdateRepo # This constant holds the name to the config file, located in ~/ CONFIG_FILE = '.updaterepo'.freeze # An encapsulated class to walk the repo directories and update all Git # repositories found therein. # rubocop:disable Metrics/ClassLength class WalkRepo include Helpers # Class constructor. No parameters required. # @return [void] def initialize @metrics = { processed: 0, skipped: 0, failed: 0, updated: 0, start_time: 0, failed_list: [] } @summary = { processed: 'green', updated: 'cyan', skipped: 'yellow', failed: 'red' } # create a new instance of the CmdConfig class then read the config var @cmd = CmdConfig.new # set up the logfile if needed setup_logfile if cmd(:log) end # This function will perform the required actions to traverse the Repo. # @example # walk_repo = UpdateRepo::WalkRepo.new # walk_repo.start def start String.disable_colorization = !cmd(:color) # make sure we dont have bad cmd-line parameter combinations ... @cmd.check_params # TODO - check this since is already called in @cmd.init # print out our header unless we are dumping / importing ... show_header unless dumping? config['location'].each do |loc| cmd(:dump_tree) ? dump_tree(File.join(loc)) : recurse_dir(loc) end # print out an informative footer unless dump / import ... footer unless dumping? end private def dumping? cmd(:dump) || cmd(:dump_remote) || cmd(:dump_tree) end # returns the Confoog class which can then be used to access any config var # @return [void] # @param [none] def config @cmd.getconfig end # Return the true value of the specified configuration parameter. This is a # helper function that simply calls the 'true_cmd' function in the @cmd # class # @param command [symbol] The defined command symbol that is to be returned # @return [various] The value of the requested command option # @example # logging = cmd(:log) def cmd(command) @cmd.true_cmd(command.to_sym) end # Set up the log file - determine if we need to timestamp the filename and # then actually open it and set to sync. # @param [none] # @return [void] def setup_logfile filename = if cmd(:timestamp) 'updaterepo-' + Time.new.strftime('%y%m%d-%H%M%S') + '.log' else 'updaterepo.log' end @logfile = File.open(filename, 'w') @logfile.sync = true end # take each directory contained in the Repo directory, if it is detected as # a Git repository then update it (or as directed by command line) # @param dirname [string] Contains the directory to search for Git repos.] # @return [void] def recurse_dir(dirname) Dir.chdir(dirname) do Dir['**/'].each do |dir| next unless gitdir?(dir) if dumping? dump_repo(File.join(dirname, dir)) else notexception?(dir) ? update_repo(dir) : skip_repo(dir) end end end end # tests to see if the given directory is an exception and should be skipped # @param dir [string] Directory to be checked # @return [boolean] True if this is NOT an exception, False otherwise def notexception?(dir) !config['exceptions'].include?(File.basename(dir)) end # Display a simple header to the console # @example # show_header # @return [void] # @param [none] def show_header # print an informative header before starting print_log "\nGit Repo update utility (v", VERSION, ')', " \u00A9 Grant Ramsay \n" print_log "Using Configuration from '#{config.config_path}'\n" # print_log "Command line is : #{config['cmd']}\n" # list out the locations that will be searched list_locations # list any exceptions that we have from the config file list_exceptions # save the start time for later display in the footer... @metrics[:start_time] = Time.now print_log "\n" # blank line before processing starts end # print out a brief footer. This will be expanded later. # @return [void] # @param [none] def footer duration = Time.now - @metrics[:start_time] print_log "\nUpdates completed in ", show_time(duration).cyan print_metrics print_log " \n\n" # close the log file now as we are done, just to be sure ... @logfile.close if @logfile end # Print end-of-run metrics to console / log # @return [void] # @param [none] def print_metrics @summary.each do |metric, color| metric_value = @metrics[metric] output = "#{metric_value} #{metric.capitalize}" print_log ' | ', output.send(color.to_sym) unless metric_value.zero? end print_log ' |' return if @metrics[:failed_list].empty? print_log "\n\n!! Note : The following repositories ", 'FAILED'.red.underline, ' during this run :' @metrics[:failed_list].each do |failed| print_log "\n [", 'x'.red, "] #{failed}" end end # Print a list of any defined expections that will not be updated. # @return [void] # @param [none] def list_exceptions exceptions = config['exceptions'] return unless exceptions # if exceptions print_log "\nExclusions:".underline, ' ', exceptions.join(', ').yellow, "\n" # end end # Print a list of all top-level directories that will be searched and any # Git repos contained within updated. # @return [void] def list_locations print_log "\nRepo location(s):\n".underline config['location'].each do |loc| print_log '-> ', loc.cyan, "\n" end end # Takes the specified Repo and does not update it, outputing a note to the # console / log to this effect. # @param dirpath [string] The directory with Git repository to be skipped # @return [void] # @example # skip_repo('/Repo/Personal/work-in-progress') def skip_repo(dirpath) Dir.chdir(dirpath.chomp!('/')) do repo_url = `git config remote.origin.url`.chomp print_log '* Skipping ', Dir.pwd.yellow, " (#{repo_url})\n" @metrics[:skipped] += 1 end end # Takes the specified Repo outputs information and the repo URL then calls # #do_update to actually update it. # @param dirname [string] The directory that will be updated # @return [void] # @example # update_repo('/Repo/linux/stable') def update_repo(dirname) Dir.chdir(dirname.chomp!('/')) do # repo_url = `git config remote.origin.url`.chomp do_update @metrics[:processed] += 1 end end # Actually perform the update of this specific repository, calling the # function #do_threads to handle the output to screen and log. # @param none # @return [void] def do_update repo_url = `git config remote.origin.url`.chomp print_log '* Checking ', Dir.pwd.green, " (#{repo_url})\n" Open3.popen3('git pull') do |stdin, stdout, stderr, thread| stdin.close do_threads(stdout, stderr, repo_url) thread.join end end # Create 2 individual threads to handle both STDOUT and STDERR streams, # writing to console and log if specified. # @param stdout [stream] STDOUT Stream from the popen3 call # @param stderr [stream] STDERR Stream from the popen3 call # @param repo_url [string] URL of the associated repository # @return [void] def do_threads(stdout, stderr, repo_url) { out: stdout, err: stderr }.each do |key, stream| Thread.new do while (line = stream.gets) if key == :err && line =~ /^fatal:|^error:/ print_log ' ', line.red @metrics[:failed] += 1 fullpath = Dir.pwd.red @metrics[:failed_list].push("#{fullpath} (#{repo_url})") else print_log ' ', line.cyan @metrics[:updated] += 1 if line =~ %r{^From\s(?:https?|git)://} end end end end end # this function will either dump out a CSV with the directory and remote, # or just the remote depending if we called --dump or --dump-remote # @param dir [string] The local directory for the repository # @return [void] def dump_repo(dir) Dir.chdir(dir.chomp!('/')) do repo_url = `git config remote.origin.url`.chomp print_log "#{trunc_dir(dir, config['cmd'][:prune])}," if cmd(:dump) print_log "#{repo_url}\n" end end # This function will recurse though all the subdirectories of the specified # directory and print only the directory name in a tree format. def dump_tree(dir) print "here for #{dir}\n" end end end