# -*- encoding : utf-8 -*- require 'fileutils' require 'thor' module Homesick # Homesick's command line interface class CLI < Thor include Thor::Actions include Homesick::Actions::FileActions include Homesick::Actions::GitActions include Homesick::Version include Homesick::Utils add_runtime_options! map '-v' => :version map '--version' => :version # Retain a mapped version of the symlink command for compatibility. map symlink: :link def initialize(args = [], options = {}, config = {}) super # Check if git is installed unless git_version_correct? say_status :error, "Git version >= #{Homesick::Actions::GitActions::STRING} must be installed to use Homesick", :red exit(1) end # Hack in support for diffing symlinks # Also adds support for checking if destination or content is a directory shell_metaclass = class << shell; self; end shell_metaclass.send(:define_method, :show_diff) do |destination, source| destination = Pathname.new(destination) source = Pathname.new(source) return 'Unable to create diff: destination or content is a directory' if destination.directory? || source.directory? return super(destination, File.binread(source)) unless destination.symlink? say "- #{destination.readlink}", :red, true say "+ #{source.expand_path}", :green, true end end desc 'clone URI CASTLE_NAME', 'Clone +uri+ as a castle with name CASTLE_NAME for homesick' def clone(uri, destination=nil) destination = Pathname.new(destination) unless destination.nil? inside repos_dir do if File.exist?(uri) uri = Pathname.new(uri).expand_path fail "Castle already cloned to #{uri}" if uri.to_s.start_with?(repos_dir.to_s) destination = uri.basename if destination.nil? ln_s uri, destination elsif uri =~ GITHUB_NAME_REPO_PATTERN destination = Pathname.new(uri).basename if destination.nil? git_clone "https://github.com/#{Regexp.last_match[1]}.git", destination: destination elsif uri =~ /%r([^%r]*?)(\.git)?\Z/ || uri =~ /[^:]+:([^:]+)(\.git)?\Z/ destination = Pathname.new(Regexp.last_match[1].gsub(/\.git$/, '')).basename if destination.nil? git_clone uri, destination: destination else fail "Unknown URI format: #{uri}" end setup_castle(destination) end end desc 'rc CASTLE', 'Run the .homesickrc for the specified castle' method_option :force, type: :boolean, default: false, desc: 'Evaluate .homesickrc without prompting.' def rc(name = DEFAULT_CASTLE_NAME) inside repos_dir do destination = Pathname.new(name) homesickrc = destination.join('.homesickrc').expand_path return unless homesickrc.exist? proceed = options[:force] || shell.yes?("#{name} has a .homesickrc. Proceed with evaling it? (This could be destructive)") return say_status 'eval skip', "not evaling #{homesickrc}, #{destination} may need manual configuration", :blue unless proceed say_status 'eval', homesickrc inside destination do eval homesickrc.read, binding, homesickrc.expand_path.to_s end end end desc 'pull CASTLE', 'Update the specified castle' method_option :all, type: :boolean, default: false, required: false, desc: 'Update all cloned castles' def pull(name = DEFAULT_CASTLE_NAME) if options[:all] inside_each_castle do |castle| say castle.to_s.gsub(repos_dir.to_s + '/', '') + ':' update_castle castle end else update_castle name end end desc 'commit CASTLE MESSAGE', "Commit the specified castle's changes" def commit(name = DEFAULT_CASTLE_NAME, message = nil) commit_castle name, message end desc 'push CASTLE', 'Push the specified castle' def push(name = DEFAULT_CASTLE_NAME) push_castle name end desc 'unlink CASTLE', 'Unsymlinks all dotfiles from the specified castle' def unlink(name = DEFAULT_CASTLE_NAME) check_castle_existance(name, 'symlink') inside castle_dir(name) do subdirs = subdirs(name) # unlink files unsymlink_each(name, castle_dir(name), subdirs) # unlink files in subdirs subdirs.each do |subdir| unsymlink_each(name, subdir, subdirs) end end end desc 'link CASTLE', 'Symlinks all dotfiles from the specified castle' method_option :force, type: :boolean, default: false, desc: 'Overwrite existing conflicting symlinks without prompting.' def link(name = DEFAULT_CASTLE_NAME) check_castle_existance(name, 'symlink') inside castle_dir(name) do subdirs = subdirs(name) # link files symlink_each(name, castle_dir(name), subdirs) # link files in subdirs subdirs.each do |subdir| symlink_each(name, subdir, subdirs) end end end desc 'track FILE CASTLE', 'add a file to a castle' def track(file, castle = DEFAULT_CASTLE_NAME) castle = Pathname.new(castle) file = Pathname.new(file.chomp('/')) check_castle_existance(castle, 'track') absolute_path = file.expand_path relative_dir = absolute_path.relative_path_from(home_dir).dirname castle_path = Pathname.new(castle_dir(castle)).join(relative_dir) FileUtils.mkdir_p castle_path # Are we already tracking this or anything inside it? target = Pathname.new(castle_path.join(file.basename)) if target.exist? if absolute_path.directory? move_dir_contents(target, absolute_path) absolute_path.rmtree subdir_remove(castle, relative_dir + file.basename) elsif more_recent? absolute_path, target target.delete mv absolute_path, castle_path else say_status(:track, "#{target} already exists, and is more recent than #{file}. Run 'homesick SYMLINK CASTLE' to create symlinks.", :blue) end else mv absolute_path, castle_path end inside home_dir do absolute_path = castle_path + file.basename home_path = home_dir + relative_dir + file.basename ln_s absolute_path, home_path end inside castle_path do git_add absolute_path end # are we tracking something nested? Add the parent dir to the manifest subdir_add(castle, relative_dir) unless relative_dir.eql?(Pathname.new('.')) end desc 'list', 'List cloned castles' def list inside_each_castle do |castle| say_status castle.relative_path_from(repos_dir).to_s, `git config remote.origin.url`.chomp, :cyan end end desc 'status CASTLE', 'Shows the git status of a castle' def status(castle = DEFAULT_CASTLE_NAME) check_castle_existance(castle, 'status') inside repos_dir.join(castle) do git_status end end desc 'diff CASTLE', 'Shows the git diff of uncommitted changes in a castle' def diff(castle = DEFAULT_CASTLE_NAME) check_castle_existance(castle, 'diff') inside repos_dir.join(castle) do git_diff end end desc 'show_path CASTLE', 'Prints the path of a castle' def show_path(castle = DEFAULT_CASTLE_NAME) check_castle_existance(castle, 'show_path') say repos_dir.join(castle) end desc 'generate PATH', 'generate a homesick-ready git repo at PATH' def generate(castle) castle = Pathname.new(castle).expand_path github_user = `git config github.user`.chomp github_user = nil if github_user == '' github_repo = castle.basename empty_directory castle inside castle do git_init if github_user url = "git@github.com:#{github_user}/#{github_repo}.git" git_remote_add 'origin', url end empty_directory 'home' end end desc 'destroy CASTLE', 'Delete all symlinks and remove the cloned repository' def destroy(name) check_castle_existance name, 'destroy' return unless shell.yes?('This will destroy your castle irreversible! Are you sure?') unlink(name) rm_rf repos_dir.join(name) end desc 'cd CASTLE', 'Open a new shell in the root of the given castle' def cd(castle = DEFAULT_CASTLE_NAME) check_castle_existance castle, 'cd' castle_dir = repos_dir.join(castle) say_status "cd #{castle_dir.realpath}", "Opening a new shell in castle '#{castle}'. To return to the original one exit from the new shell.", :green inside castle_dir do system(ENV['SHELL']) end end desc 'open CASTLE', 'Open your default editor in the root of the given castle' def open(castle = DEFAULT_CASTLE_NAME) unless ENV['EDITOR'] say_status :error, 'The $EDITOR environment variable must be set to use this command', :red exit(1) end check_castle_existance castle, 'open' castle_dir = repos_dir.join(castle) say_status "#{castle_dir.realpath}: #{ENV['EDITOR']} .", "Opening the root directory of castle '#{castle}' in editor '#{ENV['EDITOR']}'.", :green inside castle_dir do system("#{ENV['EDITOR']} .") end end desc 'exec CASTLE COMMAND', 'Execute a single shell command inside the root of a castle' def exec(castle, *args) check_castle_existance castle, 'exec' unless args.count > 0 say_status :error, 'You must pass a shell command to execute', :red exit(1) end full_command = args.join(' ') say_status "exec '#{full_command}'", "#{options[:pretend] ? 'Would execute' : 'Executing command'} '#{full_command}' in castle '#{castle}'", :green inside repos_dir.join(castle) do system(full_command) end end desc 'exec_all COMMAND', 'Execute a single shell command inside the root of every cloned castle' def exec_all(*args) unless args.count > 0 say_status :error, 'You must pass a shell command to execute', :red exit(1) end full_command = args.join(' ') inside_each_castle do |castle| say_status "exec '#{full_command}'", "#{options[:pretend] ? 'Would execute' : 'Executing command'} '#{full_command}' in castle '#{castle}'", :green system(full_command) end end desc 'version', 'Display the current version of homesick' def version say Homesick::Version::STRING end end end