├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── bin └── dit ├── dit.gemspec └── lib └── dit.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | Gemfile.lock 31 | .ruby-version 32 | .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org/" 2 | 3 | gem "thor" 4 | gem "git" 5 | gem "os" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kyle Fahringer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dit 2 | 3 | ![Dit version](https://img.shields.io/gem/v/dit.svg) 4 | 5 | Dit is a dotfile manager that hooks into git. 6 | 7 | It uses git hooks to automatically run whenever you `git commit` or `git merge`. You just keep working on that dotfiles directory as normal and dit handles the rest. 8 | 9 | Windows isn't currently supported due to a conspicious lack of symlinking on windows. Suggestions as to circumvent this restriction are welcome. 10 | 11 | ## Getting started 12 | 13 | Assuming ruby and rubygems are already installed (if not, refer to your various package managers) 14 | 15 | `gem install dit` 16 | `cd ~/my_dotfiles` 17 | `dit init` 18 | 19 | Then, use your git repository as normal. Any new files will automatically be symlinked to your home directory. 20 | 21 | ## Contributing 22 | 23 | Please do! 24 | 25 | ------------------------------ 26 | 27 | [![forthebadge](http://forthebadge.com/images/badges/built-with-ruby.svg)](http://forthebadge.com) 28 | [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) 29 | [![forthebadge](http://forthebadge.com/images/badges/uses-badges.svg)](http://forthebadge.com) 30 | -------------------------------------------------------------------------------- /bin/dit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'dit' 4 | DitCMD.start(ARGV) 5 | -------------------------------------------------------------------------------- /dit.gemspec: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'dit' 5 | s.version = '0.4' 6 | s.date = Date.today.strftime('%Y-%m-%d') 7 | s.summary = "Dit is a dotfiles manager that thinks it's git." 8 | s.description = 'Dit is a dotfiles manager that wraps around git and makes ' \ 9 | 'dotfiles easy to manage across devices.' 10 | s.authors = ['Kyle Fahringer'] 11 | s.files = ['lib/dit.rb'] 12 | s.executables << 'dit' 13 | s.license = 'MIT' 14 | s.homepage = 'http://github.com/vulpino/dit' 15 | s.email = 'hispanic@hush.ai' 16 | s.add_runtime_dependency 'thor', '~> 0.19.1' 17 | s.add_runtime_dependency 'git', '~> 1.2', '>= 1.2.9' 18 | s.add_runtime_dependency 'os', '~> 0.9.6' 19 | end 20 | -------------------------------------------------------------------------------- /lib/dit.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'git' 3 | require 'os' 4 | require 'fileutils' 5 | require 'set' 6 | 7 | # This is the class where all the dit work is done. 8 | # The thor class is basically a very thin layer on top of this that just 9 | # calls its methods directly. 10 | # This is because the hooks are not running through the Thor object, but also 11 | # referencing these methods. 12 | class Dit 13 | def self.init 14 | exit_if_windows 15 | 16 | if Dir.exist?('.git') 17 | symlink_all if prompt_for_symlink_all 18 | else 19 | Git.init(Dir.getwd) 20 | puts "Initialized empty Git repository in #{File.join(Dir.getwd, '.git')}" 21 | end 22 | 23 | hook 24 | 25 | puts 'Dit was successfully hooked into .git/hooks.' 26 | end 27 | 28 | def self.exit_if_windows 29 | if OS.windows? 30 | puts 'This is a windows system, and dit does not support windows.' 31 | puts 'See vulpino/dit issue #1 if you have a potential solution.' 32 | exit 1 33 | end 34 | end 35 | 36 | def self.prompt_for_symlink_all 37 | puts 'Dit has detected an existing git repo, and will initialize it to ' \ 38 | 'populate your ~ directory with symlinks.' 39 | puts 'Please confirm this by typing y, or anything else to cancel.' 40 | response = STDIN.gets.chomp.upcase 41 | response == 'Y' 42 | end 43 | 44 | def self.hook 45 | Dir.chdir(File.join('.git', 'hooks')) do 46 | # The following check for the existence of post-commit or post-merge hooks 47 | # and will not interfere with them if they exist and do not use bash. 48 | append_to_post_commit, cannot_post_commit = detect_hook 'post-commit' 49 | append_to_post_merge, cannot_post_merge = detect_hook 'post-merge' 50 | 51 | write_hook('post-commit', append_to_post_commit) unless cannot_post_commit 52 | write_hook('post-merge', append_to_post_merge) unless cannot_post_merge 53 | 54 | make_dit 55 | make_ruby_enforcer 56 | 57 | # Make sure they're executable 58 | FileUtils.chmod '+x', %w(post-commit post-merge dit force-ruby) 59 | end 60 | end 61 | 62 | def self.symlink_list(list) 63 | list = get_roots list 64 | list.each do |f| 65 | wd_f = File.expand_path f 66 | home_f = File.expand_path(f).gsub(Dir.getwd, Dir.home) 67 | symlink wd_f, home_f 68 | end 69 | end 70 | 71 | def self.get_roots(list) 72 | root_list = Set[] 73 | list.each do |f| 74 | f.strip! 75 | root = f.split('/')[0] 76 | root ||= f 77 | root_list |= Set[root] 78 | end 79 | root_list.delete?('') 80 | %w(.gitignore README.md README).each { |i| root_list.delete(i) } 81 | root_list 82 | end 83 | 84 | def self.symlink_unlinked 85 | symlink_list `git show --pretty="format:" --name-only HEAD`.split("\n") 86 | end 87 | 88 | def self.symlink_all 89 | current_branch = `git rev-parse --abbrev-ref HEAD`.chomp 90 | symlink_list `git ls-tree -r #{current_branch} --name-only`.split("\n") 91 | end 92 | 93 | def self.symlink(a, b) 94 | if File.exist?(b) 95 | return if File.symlink?(b) && File.readlink(b).include?(Dir.getwd) 96 | return unless prompt_for_overwrite a, b 97 | end 98 | File.symlink(a, b) 99 | rescue 100 | puts "Failed to symlink #{a} to #{b}" 101 | end 102 | 103 | def self.prompt_for_overwrite(a, b) 104 | return false if @never 105 | (FileUtils.rm(b); return true) if @always # just this once; 106 | puts "#{b} conflicts with #{a}. Remove #{b}? [y/n/a/s]" 107 | puts "To always overwrite, type \"A\". To never overwrite, type \"S\"" 108 | response = STDIN.gets.upcase 109 | case response 110 | when 'Y' 111 | FileUtils.rm(b) 112 | return true 113 | when 'A' 114 | @always = true 115 | FileUtils.rm(b) 116 | return true 117 | when 'S' 118 | @never = true 119 | end 120 | false 121 | end 122 | 123 | def self.detect_hook(hook) 124 | return [false, false] unless File.exist?(hook) 125 | 126 | cannot_hook, append_to_hook = false 127 | 128 | if `cat #{hook}`.include?('./.git/hooks/dit') 129 | puts 'Dit hook already installed.' 130 | cannot_hook = true 131 | elsif `cat #{hook}`.include?('#!/usr/bin/env bash') 132 | puts "You have #{hook} hooks already that use bash, so we'll " \ 133 | 'append ourselves to the file.' 134 | append_to_hook = true 135 | else 136 | puts "You have #{hook} hooks that use some foreign language, " \ 137 | "so we won't interfere, but we can't hook in there." 138 | cannot_hook = true 139 | end 140 | 141 | [append_to_hook, cannot_hook] 142 | end 143 | 144 | def self.write_hook(hook_file, do_append) 145 | File.open(hook_file, 'a') do |f| 146 | f.puts '#!/usr/bin/env bash' unless do_append 147 | f.puts '( exec ./.git/hooks/dit )' 148 | end 149 | end 150 | 151 | def self.make_dit 152 | File.open('dit', 'a') do |f| 153 | f.puts '#!/usr/bin/env ./.git/hooks/force-ruby' 154 | f.puts "require 'dit'" 155 | f.puts 'Dit.symlink_unlinked' 156 | end 157 | end 158 | 159 | def self.make_ruby_enforcer 160 | # The following lines are because git hooks do this weird thing 161 | # where they prepend /usr/bin to the path and a bunch of other stuff 162 | # meaning git hooks will use /usr/bin/ruby instead of any ruby 163 | # from rbenv or rvm or chruby, so we make a script forcing the hook 164 | # to use our ruby 165 | ruby_path = `which ruby` 166 | if ruby_path != '/usr/bin/ruby' 167 | ruby_folder = File.dirname(ruby_path) 168 | File.open('force-ruby', 'a') do |f| 169 | f.puts '#!/usr/bin/env bash' 170 | f.puts 'set -e' 171 | f.puts 'PATH=#{ruby_folder}:$PATH' 172 | f.puts "exec ruby \"$@\"" 173 | end 174 | else 175 | File.open('force-ruby', 'a') do |f| 176 | f.puts '#!/usr/bin/env bash' 177 | f.puts "exec ruby \"$@\"" 178 | end 179 | end 180 | end 181 | 182 | def self.clean_home 183 | Dir.chdir(Dir.home) do 184 | existing_dotfiles = Dir.glob('.*') 185 | existing_dotfiles.each do |f| 186 | next if f == '.' || f == '..' 187 | if File.symlink?(f) 188 | f_abs = File.readlink(f) 189 | File.delete(f) unless File.exist?(f_abs) 190 | end 191 | end 192 | end 193 | end 194 | 195 | def self.version 196 | '0.4' 197 | end 198 | end 199 | 200 | # This is the thor class the CLI calls. 201 | # It's a thin layer on top of the Dit class. See above. 202 | class DitCMD < Thor 203 | desc 'init', 'Initialize the current directory as a dit directory.' 204 | def init 205 | Dit.init 206 | end 207 | 208 | desc 'rehash', "Manually symlink everything in case a git hook didn't run." 209 | def rehash 210 | Dit.symlink_all 211 | end 212 | 213 | desc 'version', 'Print the dit version.' 214 | def version 215 | puts "Dit #{Dit.version} on ruby #{RUBY_VERSION}" 216 | end 217 | 218 | desc 'clean', 'Clean dead symlinks from your home dir.' 219 | def clean 220 | Dit.clean_home 221 | end 222 | end 223 | --------------------------------------------------------------------------------