diff --git a/.gitignore b/.gitignore index 88cf2e9..e605e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ pkg # # For vim: *.swp +# +# For IDEA: +.idea/ +*.iml diff --git a/.travis.yml b/.travis.yml index e14e9bc..285725e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby rvm: + - 2.0.0 - 1.9.3 - 1.8.7 diff --git a/README.markdown b/README.markdown index a0d239d..2ba35c8 100644 --- a/README.markdown +++ b/README.markdown @@ -51,6 +51,84 @@ Not sure what else homesick has up its sleeve? There's always the built in help: homesick help +## .homesick_subdir + +`homesick symlink` basically makes symlink to only first depth in `castle/home`. If you want to link nested files/directories, please use .homesick_subdir. + +For example, when you have castle like this: + + castle/home + `-- .config + `-- fooapp + |-- config1 + |-- config2 + `-- config3 + +and have home like this: + + $ tree -a + ~ + |-- .config + | `-- barapp + | |-- config1 + | |-- config2 + | `-- config3 + `-- .emacs.d + |-- elisp + `-- inits + +You may want to symlink only to `castle/home/.config/fooapp` instead of `castle/home/.config` because you already have `~/.config/barapp`. In this case, you can use .homesick_subdir. Please write "directories you want to look up sub direcoties (instead of just first depth)" in this file. + +castle/home/.homesick_subdir + + .config + +and run `homesick symlink CASTLE`. The result is: + + ~ + |-- .config + | |-- barapp + | | |-- config1 + | | |-- config2 + | | `-- config3 + | `-- fooapp -> castle/home/.config/fooapp + `-- .emacs.d + |-- elisp + `-- inits + +Or `homesick track NESTED_FILE CASTLE` adds a line automatically. For example: + + homesick track .emacs.d/elisp castle + +castle/home/.homesick_subdir + + .config + .emacs.d + +home directory + + ~ + |-- .config + | |-- barapp + | | |-- config1 + | | |-- config2 + | | `-- config3 + | `-- fooapp -> castle/home/.config/fooapp + `-- .emacs.d + |-- elisp -> castle/home/.emacs.d/elisp + `-- inits + +and castle + + castle/home + |-- .config + | `-- fooapp + | |-- config1 + | |-- config2 + | `-- config3 + `-- .emacs.d + `-- elisp + ## Note on Patches/Pull Requests * Fork the project. diff --git a/lib/homesick.rb b/lib/homesick.rb index 341e3f9..cc03e81 100644 --- a/lib/homesick.rb +++ b/lib/homesick.rb @@ -10,6 +10,7 @@ class Homesick < Thor add_runtime_options! GITHUB_NAME_REPO_PATTERN = /\A([A-Za-z_-]+\/[A-Za-z_-]+)\Z/ + SUBDIR_FILENAME = ".homesick_subdir" def initialize(args=[], options={}, config={}) super @@ -96,15 +97,14 @@ class Homesick < Thor check_castle_existance(name, "symlink") inside castle_dir(name) do - files = Pathname.glob('{.*,*}').reject{|a| [".",".."].include?(a.to_s)} - files.each do |path| - absolute_path = path.expand_path + subdirs = subdirs(name) - inside home_dir do - adjusted_path = (home_dir + path).basename + # link files + symlink_each(name, castle_dir(name), subdirs) - ln_s absolute_path, adjusted_path - end + # link files in subdirs + subdirs.each do |subdir| + symlink_each(name, subdir, subdirs) end end end @@ -112,22 +112,46 @@ class Homesick < Thor desc "track FILE CASTLE", "add a file to a castle" def track(file, castle) castle = Pathname.new(castle) - file = Pathname.new(file) + file = Pathname.new(file.chomp('/')) check_castle_existance(castle, 'track') absolute_path = file.expand_path - castle_path = castle_dir(castle) - mv absolute_path, castle_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 + shell.say_status(:track, "#{target} already exists, and is more recent than #{file}. Run 'homesick SYMLINK CASTLE' to create symlinks.", :blue) unless options[:quiet] + end + else + mv absolute_path, castle_path + end inside home_dir do - absolute_path = castle_dir(castle) + file.basename - home_path = home_dir + file + 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 + unless relative_dir.eql?(Pathname.new('.')) + subdir_add(castle, relative_dir) + end end desc "list", "List cloned castles" @@ -219,4 +243,99 @@ class Homesick < Thor git_push end end + + def subdir_file(castle) + repos_dir.join(castle, SUBDIR_FILENAME) + end + + def subdirs(castle) + subdir_filepath = subdir_file(castle) + subdirs = [] + if subdir_filepath.exist? + subdir_filepath.readlines.each do |subdir| + subdirs.push(subdir.chomp) + end + end + subdirs + end + + def subdir_add(castle, path) + subdir_filepath = subdir_file(castle) + File.open(subdir_filepath, 'a+') do |subdir| + subdir.puts path unless subdir.readlines.inject(false) { |memo, line| line.eql?("#{path.to_s}\n") || memo } + end + + inside castle_dir(castle) do + git_add subdir_filepath + end + end + + def subdir_remove(castle, path) + subdir_filepath = subdir_file(castle) + if subdir_filepath.exist? + lines = IO.readlines(subdir_filepath).delete_if { |line| line == "#{path}\n" } + File.open(subdir_filepath, 'w') { |manfile| manfile.puts lines } + end + + inside castle_dir(castle) do + git_add subdir_filepath + end + end + + def move_dir_contents(target, dir_path) + child_files = dir_path.children + child_files.each do |child| + + target_path = target.join(child.basename) + if target_path.exist? + if more_recent?(child, target_path) && target.file? + target_path.delete + mv child, target + end + next + end + + mv child, target + end + end + + def more_recent?(first, second) + first_p = Pathname.new(first) + second_p = Pathname.new(second) + first_p.mtime > second_p.mtime && !first_p.symlink? + end + + def symlink_each(castle, basedir, subdirs) + absolute_basedir = Pathname.new(basedir).expand_path + inside basedir do + files = Pathname.glob('{.*,*}').reject{|a| [".", ".."].include?(a.to_s)} + files.each do |path| + absolute_path = path.expand_path + castle_home = castle_dir(castle) + + # make ignore dirs + ignore_dirs = [] + subdirs.each do |subdir| + # ignore all parent of each line in subdir file + Pathname.new(subdir).ascend do |p| + ignore_dirs.push(p) + end + end + + # ignore dirs written in subdir file + matched = false + ignore_dirs.uniq.each do |ignore_dir| + if absolute_path == castle_home.join(ignore_dir) + matched = true + break + end + end + next if matched + + relative_dir = absolute_basedir.relative_path_from(castle_home) + home_path = home_dir.join(relative_dir).join(path) + ln_s absolute_path, home_path + end + end + end end diff --git a/lib/homesick/actions.rb b/lib/homesick/actions.rb index 9e5d903..82c4036 100644 --- a/lib/homesick/actions.rb +++ b/lib/homesick/actions.rb @@ -93,6 +93,7 @@ class Homesick def ln_s(source, destination, config = {}) source = Pathname.new(source) destination = Pathname.new(destination) + FileUtils.mkdir_p destination.dirname if destination.symlink? if destination.readlink == source diff --git a/spec/homesick_spec.rb b/spec/homesick_spec.rb index 81fafb7..0d5c83a 100644 --- a/spec/homesick_spec.rb +++ b/spec/homesick_spec.rb @@ -120,12 +120,59 @@ describe "homesick" do existing_dotdir_link.readlink.should == dotdir end end + + context "with '.config' in .homesick_subdir" do + let(:castle) { given_castle("glencairn", [".config"]) } + it "can symlink in sub directory" do + dotdir = castle.directory(".config") + dotfile = dotdir.file(".some_dotfile") + + homesick.symlink("glencairn") + + home_dotdir = home.join(".config") + home_dotdir.symlink?.should == false + home_dotdir.join(".some_dotfile").readlink.should == dotfile + end + end + + context "with '.config/appA' in .homesick_subdir" do + let(:castle) { given_castle("glencairn", [".config/appA"]) } + it "can symlink in nested sub directory" do + dotdir = castle.directory(".config").directory("appA") + dotfile = dotdir.file(".some_dotfile") + + homesick.symlink("glencairn") + + home_dotdir = home.join(".config").join("appA") + home_dotdir.symlink?.should == false + home_dotdir.join(".some_dotfile").readlink.should == dotfile + end + end + + context "with '.config' and '.config/appA' in .homesick_subdir" do + let(:castle) { given_castle("glencairn", [".config", ".config/appA"]) } + it "can symlink under both of .config and .config/appA" do + config_dir = castle.directory(".config") + config_dotfile = config_dir.file(".some_dotfile") + appA_dir = config_dir.directory("appA") + appA_dotfile = appA_dir.file(".some_appfile") + + homesick.symlink("glencairn") + + home_config_dir = home.join(".config") + home_appA_dir = home_config_dir.join("appA") + home_config_dir.symlink?.should == false + home_config_dir.join(".some_dotfile").readlink.should == config_dotfile + home_appA_dir.symlink?.should == false + home_appA_dir.join(".some_appfile").readlink.should == appA_dotfile + end + end end describe "list" do it "should say each castle in the castle directory" do given_castle('zomg') - given_castle('zomg', 'wtf/zomg') + given_castle('wtf/zomg') homesick.should_receive(:say_status).with("zomg", "git://github.com/technicalpickles/zomg.git", :cyan) homesick.should_receive(:say_status).with("wtf/zomg", "git://github.com/technicalpickles/zomg.git", :cyan) @@ -169,5 +216,70 @@ describe "homesick" do some_rc_file.readlink.should == tracked_file end + + it 'should track a file in nested folder structure' do + castle = given_castle('castle_repo') + + some_nested_file = home.file('some/nested/file.txt') + homesick.track(some_nested_file.to_s, 'castle_repo') + + tracked_file = castle.join('some/nested/file.txt') + tracked_file.should exist + some_nested_file.readlink.should == tracked_file + end + + it 'should track a nested directory' do + castle = given_castle('castle_repo') + + some_nested_dir = home.directory('some/nested/directory/') + homesick.track(some_nested_dir.to_s, 'castle_repo') + + tracked_file = castle.join('some/nested/directory/') + tracked_file.should exist + some_nested_dir.realpath.should == tracked_file.realpath + end + + describe "subdir_file" do + + it 'should add the nested files parent to the subdir_file' do + castle = given_castle('castle_repo') + + some_nested_file = home.file('some/nested/file.txt') + homesick.track(some_nested_file.to_s, 'castle_repo') + + subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME) + File.open(subdir_file, 'r') do |f| + f.readline.should == "some/nested\n" + end + end + + it 'should NOT add anything if the files parent is already listed' do + castle = given_castle('castle_repo') + + some_nested_file = home.file('some/nested/file.txt') + other_nested_file = home.file('some/nested/other.txt') + homesick.track(some_nested_file.to_s, 'castle_repo') + homesick.track(other_nested_file.to_s, 'castle_repo') + + subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME) + File.open(subdir_file, 'r') do |f| + f.readlines.size.should == 1 + end + end + + it 'should remove the parent of a tracked file from the subdir_file if the parent itself is tracked' do + castle = given_castle('castle_repo') + + some_nested_file = home.file('some/nested/file.txt') + nested_parent = home.directory('some/nested/') + homesick.track(some_nested_file.to_s, 'castle_repo') + homesick.track(nested_parent.to_s, 'castle_repo') + + subdir_file = castle.parent.join(Homesick::SUBDIR_FILENAME) + File.open(subdir_file, 'r') do |f| + f.each_line { |line| line.should_not == "some/nested\n" } + end + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 498bbab..1c78cf3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,11 +16,18 @@ RSpec.configure do |config| homesick.stub(:say_status) end - def given_castle(name, path=name) + def given_castle(path, subdirs=[]) + name = Pathname.new(path).basename castles.directory(path) do |castle| Dir.chdir(castle) do system "git init >/dev/null 2>&1" system "git remote add origin git://github.com/technicalpickles/#{name}.git >/dev/null 2>&1" + if subdirs then + subdir_file = castle.join(Homesick::SUBDIR_FILENAME) + subdirs.each do |subdir| + system "echo #{subdir} >> #{subdir_file}" + end + end return castle.directory("home") end end