├── LICENSE ├── git-fuzzyadd.rb └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by David Verhasselt (david@crowdway.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /git-fuzzyadd.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Searches changed but not updated files for matches with the 4 | # given fuzzy pattern and adds them to the git repo. 5 | # 6 | # Copyright (c) 2011 by David Verhasselt (david@crowdway.com) 7 | # 8 | # Licensed under the MIT License. See included file LICENSE 9 | # 10 | 11 | class FuzzyGitAdd 12 | def initialize 13 | end 14 | 15 | def run 16 | if ARGV.count == 0 17 | no_change_death 18 | end 19 | 20 | ARGV.each do |pattern| 21 | files = find_first_matches(modified_files, pattern) 22 | 23 | if files.nil? 24 | puts "#{pattern} matches no files." 25 | no_change_death 26 | 27 | elsif files.count > 1 28 | puts "#{pattern} matches #{files.count} files, please specify:" 29 | files.each do |file| 30 | puts " #{file}\n" 31 | end 32 | no_change_death 33 | 34 | else 35 | puts "added #{files[0]}." 36 | system "git add #{files[0]}" 37 | end 38 | end 39 | end 40 | 41 | private 42 | # Returns array with all changed but not updated files 43 | # ["filename3.txt"] 44 | def modified_files 45 | `git status -sz`.scan(/ M ([^\0]+)\0/) 46 | end 47 | 48 | # Returns array as follows: 49 | # [ 50 | # ["M", " ", "filename1.txt"], 51 | # ["?", "?", "filename2.txt"], 52 | # [" ", "M", "filename3.txt"] 53 | # ] 54 | def status_files 55 | `git status -sz`.scan(/([^\0])([^\0]) ([^\0]+)\0/) 56 | end 57 | 58 | def find_first_matches(files, pattern) 59 | matches = [] 60 | 61 | (0..5).each do |stage| 62 | regexp = create_fuzzy_regexp(pattern, stage) 63 | files.each do |file| 64 | if file[0].match regexp 65 | matches << file[0] 66 | end 67 | end 68 | 69 | unless matches.empty? 70 | return matches 71 | end 72 | end 73 | end 74 | 75 | 76 | def create_fuzzy_regexp(pattern, stage = 0) 77 | path_re = "" 78 | filename = pattern 79 | 80 | =begin 81 | stage 0: no wildcards (whole word) 82 | stage 1: wildcards around directory names, no wildcards around filename 83 | stage 2: wildcards around directory names, wildcards around whole filename (no slashes after filename) 84 | stage 3: wildcards around directory names, wildcards around whole filename 85 | stage 4: wildcards around directory names, wildcard in front of filename and around extension-dot 86 | stage 5: wildcards around every character 87 | =end 88 | 89 | if pattern.index("/") and (1..3).contains(stage) 90 | path = filename.split("/") 91 | filename = path.pop 92 | 93 | path.each do |dir| 94 | path_re += ".*#{dir}.*/" 95 | end 96 | end 97 | 98 | case stage 99 | when 0 100 | /#{pattern}/ 101 | 102 | when 1 103 | /#{path_re}#{filename}/ 104 | 105 | when 2 106 | /#{path_re}.*#{filename}[^\/]*/ 107 | 108 | when 3 109 | /#{path_re}.*#{filename}.*/ 110 | 111 | when 4 112 | if not filename.index(".") 113 | create_fuzzy_regexp(pattern, 3) 114 | else 115 | path_re += ".*\..*" + filename.split(".").join(".*\..*") 116 | /#{path_re}/ 117 | end 118 | 119 | when 5 120 | regexp = "" 121 | pattern.each_char do |c| 122 | regexp += Regexp.escape(c) + ".*" 123 | end 124 | /#{regexp}/ 125 | end 126 | end 127 | 128 | def no_change_death 129 | puts "nothing happened." 130 | exit 131 | end 132 | end 133 | 134 | if __FILE__ == $0 135 | FuzzyGitAdd.new.run 136 | end 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fuzzy Git 2 | ========= 3 | 4 | These are a series of scripts that extend common git commands with fuzzy finder functionality. For now, only `git-fuzzyadd` exists. 5 | 6 | Installation 7 | ============ 8 | 9 | Create a symbolic link `/usr/bin/git-fuzzyadd` pointing to `git-fuzzyadd.rb`, e.g.: 10 | 11 | $ sudo ln -s ~/fuzzygit/git-fuzzyadd.rb /usr/bin/git-fuzzyadd 12 | 13 | Make sure the script is executable: 14 | 15 | $ sudo chmod a+x ~/fuzzygit/git-fuzzyadd.rb 16 | 17 | To create a shorter alias for git-fuzzyadd, edit `~/.gitconfig`: 18 | 19 | ... 20 | 21 | [alias] 22 | ... 23 | fa = fuzzyadd 24 | 25 | ... 26 | 27 | This will allow you to do as follows: 28 | 29 | $ git fa 30 | 31 | 32 | git-fuzzyadd 33 | ------------ 34 | 35 | Usage: 36 | 37 | $ git fuzzyadd 38 | 39 | A fuzzy git-add. Use it to add changed but not updated files that are already tracked. Fuzzy-Git uses staged fuzzy matching. It'll try to find files in decreasing order of specificity. It'll first try to match the pattern whole. If there are any slashes inside the given pattern, it'll try and match the directories, each as a whole word next. After that wildcards are added around the extension if any. If still nothing matches, any file which contains the characters in `pattern` in the same order, matches, even if there are other characters in between. If a given pattern matches multiple files, it will exit without doing anything. 40 | 41 | git-fuzzyadd accepts multiple parameters and will consider each as an independent pattern to search for. 42 | 43 | Example: 44 | 45 | david@Seven:~/example$ git status 46 | # On branch master 47 | # Changed but not updated: 48 | # (use "git add ..." to update what will be committed) 49 | # (use "git checkout -- ..." to discard changes in working directory) 50 | # 51 | # modified: app/controller/posts_controller.rb 52 | # modified: app/controller/user_controller.rb 53 | # 54 | # Untracked files: 55 | # (use "git add ..." to include in what will be committed) 56 | # 57 | # app/controller/comments_controller.rb 58 | # app/model/posts.rb 59 | no changes added to commit (use "git add" and/or "git commit -a") 60 | 61 | Notice that there are two tracked files that are changed but not yet updated, and also two untracked files. 62 | 63 | We'll try to add `posts_controller.rb`: 64 | 65 | david@Seven:~/example$ git fuzzyadd controller 66 | controller matches 2 files, please specify: 67 | app/controller/posts_controller.rb 68 | app/controller/user_controller.rb 69 | nothing happened. 70 | 71 | `controller` also matches `app/controller/user_controller.rb`, so we need to be more specific because for security reasons fuzzyadd will only work when precisely one file matches: 72 | 73 | david@Seven:~/example$ git fuzzyadd post 74 | added app/controller/posts_controller.rb 75 | 76 | Let's look at the status now: 77 | 78 | david@Seven:~example$ git status 79 | # On branch master 80 | # Changes to be committed: 81 | # (use "git reset HEAD ..." to unstage) 82 | # 83 | # modified: app/controller/posts_controller.rb 84 | # 85 | # Changed but not updated: 86 | # (use "git add ..." to update what will be committed) 87 | # (use "git checkout -- ..." to discard changes in working directory) 88 | # 89 | # modified: app/controller/user_controller.rb 90 | # 91 | # Untracked files: 92 | # (use "git add ..." to include in what will be committed) 93 | # 94 | # app/controller/comments_controller.rb 95 | # app/model/posts.rb 96 | 97 | This is what we intuitivily expect to happen. If we try the exact same command again, we get a strange result: 98 | 99 | david@Seven:~/example$ git fuzzyadd post 100 | added app/controller/user_controller.rb. 101 | 102 | Strange, but if you check, `post` indeed matches the file: *a**p**p/c**o**ntroller/u**s**er_con**t**roller.rb*. Why didn't it match this file the previous time? Because to Fuzzy-Git it was obvious that we meant posts_controller.rb and not some esoteric matching in user_controller.rb. 103 | --------------------------------------------------------------------------------