Skip to content

Instantly share code, notes, and snippets.

@MrJoy
Created October 10, 2012 22:42
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrJoy/3868993 to your computer and use it in GitHub Desktop.
Save MrJoy/3868993 to your computer and use it in GitHub Desktop.
A VERY partial implementation of .gitignore semantics in Ruby...
#!/usr/bin/env ruby
require 'set'
# This code is meant to demonstrate the difficulty of actually applying .gitignore semantics manually. It supports a key subset of .gitignore
# behaviors, including:
# * Anchored and unanchored patterns/globs.
# * Un-ignoring of patterns/globs, with the same quirky semantics as git.
# * Escaping a leading hash in a pattern.
# * User-wide exclusion list.
# * Top-of-repo .gitignore.
# TODO: Determine git configuration wrt case-folding, act accordingly. This code is set for how git behaves by default on OSX (a case-preserving FS).
# TODO: This doesn't behave properly wrt tracked vs. untracked files.
# TODO: This doesn't consider .git/info/exclude, or per-sub-directory .gitignore files.
# TODO: I haven't even looked at potential character set issues.
# TODO: The tests I've applied to this may not be comprehensive of even the cases I support.
FNMATCH_OPTIONS_UNANCHORED=File::FNM_PATHNAME|File::FNM_CASEFOLD
FNMATCH_OPTIONS_ANCHORED=File::FNM_CASEFOLD
def relevant_files_for_dir(dirname)
return Dir.glob("#{dirname}/**/*", File::FNM_DOTMATCH).
reject { |f| f =~ /(.*\/)?\.\.?$/ || f =~ /(^|\/)\.git(\/|$)/ }.
sort.
map { |f| f[(dirname.length + 1)..-1] }
end
def apply_pattern_set(rule, fname, is_anchored, is_directory_match)
patterns = [
rule,
File.join(rule, "**", "*"),
]
if(!is_anchored)
patterns += patterns.map { |p| File.join("**", p) }
matcher = proc { |pattern| File.fnmatch(pattern, fname, FNMATCH_OPTIONS_UNANCHORED) }
else
patterns << File.join(rule, "*")
matcher = proc { |pattern| File.fnmatch(pattern, fname, FNMATCH_OPTIONS_ANCHORED) }
end
matches = !!(patterns.detect &matcher)
if(is_directory_match)
remainder = fname
matches = false
while(remainder != '' && remainder != '.')
if(File.directory?(remainder) && remainder.end_with?(rule))
matches = true
break
end
remainder = File.dirname(remainder)
end
end
return matches
end
def apply_gitignore_rules(basedir, ignore_rules, relevant_files)
remaining_files = Set.new
ignored_trees = Set.new
relevant_files.each do |fname|
log "Checking: #{fname}"
should_discard = false
ignore_rules.each do |rule|
is_anchored = !!(rule =~ /\//) && (rule.index("/") != (rule.length - 1))
is_negative = !!(rule =~ /^!/)
is_directory_match = !!(rule =~ /\/$/)
rule = rule.sub(/^!/, '').sub(/^\//, '').sub(/\/$/, '').sub(/^\\#/, '#')
if(!is_negative)
new_should_discard = apply_pattern_set(rule, fname, is_anchored, is_directory_match)
should_discard ||= new_should_discard
log ">>> #{rule}; #{new_should_discard}" if(new_should_discard)
ignored_trees << fname if(should_discard && File.directory?(fname))
else
should_not_discard = apply_pattern_set(rule, fname, is_anchored, is_directory_match)
should_not_discard = false if(ignored_trees.detect { |dir| fname.start_with?("#{dir}/") })
should_discard = false if(should_not_discard)
log "<<< #{rule}; #{should_not_discard}" if(should_not_discard)
end
end
log "::: #{should_discard}; #{File.directory?(fname)}; #{File.file?(fname)}"
remaining_files << fname if(!should_discard && File.file?(fname))
end
return remaining_files.to_a
end
def tracked_files_for_dir(dirname)
remaining_files = relevant_files_for_dir(dirname)
gitignore_path = File.join(dirname, '.gitignore')
ignore_rules = []
global_gitignore_path = `git config --global --get core.excludesfile`.strip
if(File.exists?(global_gitignore_path))
# Load the user-wide gitignore, rejecting comment-lines, and
# empty/whitespace-only lines. Note that comments cannot appear on the
# same line as a pattern.
ignore_rules += File.read(global_gitignore_path).split(/\r?\n/).
reject{ |f| f =~ /^(#.*|\s*)$/ }
end
if(File.exists?(gitignore_path))
# Load the .gitignore for this directory...
ignore_rules += File.read(gitignore_path).split(/\r?\n/).
reject{ |f| f =~ /^(#.*|\s*)$/ }
end
remaining_files = apply_gitignore_rules(dirname, ignore_rules, remaining_files)
return remaining_files
end
puts tracked_files_for_dir(".").join("\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment