module Amp
  module Repositories
    
    ##
    # An entry in the dirstate. Similar to IndexEntry for revlogs. Simple struct, that's
    # all.
    class DirStateEntry < Struct.new(:status, :mode, :size, :mtime)
      
      ##
      # shortcuts!
      def removed?;     self.status == :removed; end
      def added?;       self.status == :added; end
      def untracked?;   self.status == :untracked; end
      def modified?;    self.status == :modified; end
      def merged?;      self.status == :merged; end
      def normal?;      self.status == :normal; end
      def forgotten?;   self.status == :forgotten; end
    
      ##
      # Do I represent a dirty object?
      #
      # @return [Boolean] does this array represent a dirty object in a DirState?
      def dirty?
        self[-2] == -2 && self[-1] == -1 && self.normal?
      end

      ##
      # Do I possibly represent a dirty object?
      #
      # @return [Boolean] does this array possibly represent a dirty object in a DirState?
      def maybe_dirty?
        self[-2] == -1 && self[-1] == -1 && self.normal?
      end
    end
  
    ##
    # = DirState
    # This class handles parsing and manipulating the "dirstate" file, which is stored
    # in the .hg folder. This file handles which files are marked for addition, removal,
    # copies, and so on. The structure of each entry is below.
    #
    #
    # class DirStateEntry < BitStruct
    #   default_options :endian => :network
    #   
    #   char    :status     ,  8, "the state of the file"
    #   signed  :mode       , 32, "mode"
    #   signed  :size       , 32, "size"
    #   signed  :mtime      , 32, "mtime"
    #   signed  :fname_size , 32, "filename size"
    #
    # end
    class DirState
      include Ignore
      include RevlogSupport::Node
    
      UNKNOWN = DirStateEntry.new(:untracked, 0, 0, 0)
      FORMAT  = "cNNNN"
    
      class FileNotInRootError < StandardError; end
      class AbsolutePathNeededError < StandardError; end
    
      # The parents of the current state. If there's been an uncommitted merge,
      # it will be two. Otherwise it will just be one parent and +NULL_ID+
      attr_reader :parents
    
      # The number of directories in each base ["dir" => #_of_dirs]
      attr_reader :dirs
    
      # The files mapped to their stats (state, mode, size, mtime)
      # [state, mode, size, mtime]
      attr_reader :files
    
      # A map of files to be copied, because we want to preserve their history
      # "source" => "dest"
      attr_reader :copy_map
    
      # I still don't know what this does
      attr_reader :folds
    
      # The conglomerate config object of global configs and the repo
      # specific config.
      attr_reader :config
    
      # The root of the repository
      attr_reader :root
    
      # The opener to access files. The only files that will be touched lie
      # in the .hg/ directory, so the default MUST be +:open_hg+.
      attr_reader :opener
    
      ##
      # Creates a DirState object. This is used to represent, in memory (and
      # occasionally on file) how the repository is being changed.
      # It's really simple, and it is really the basis for _using_ the repo
      # (contrary to how Revlog is the basis for _saving_ the repo).
      # 
      # @param [String] root the absolute path to the root of the repository
      # @param [Amp::AmpConfig] config the config file of hgrc
      # @param [Amp::Opener] opener the opener to open files with
      def initialize(root, config, opener)
        unless root[0, 1] == "/"
          raise AbsolutePathNeededError, "#{root} is not an absolute path!" 
        end
      
        # root must be an aboslute path with no ending slash
        @root  = root[-1, 1] == "/" ? root[0..-2] : root # the root of the repo
        @config = config # the config file where we get defaults
        @opener = opener # opener to retrieve files (default: open_hg)
        @dirty = false # has something changed, and do we need to write?
        @dirty_parents = false
        @parents = [NULL_ID, NULL_ID] # the parent revisions
        @dirs  = {} # number of directories in each base ["dir" => #_of_dirs]
        @files = {} # the files mapped to their statistics
        @copy_map = {} # src => dest
        @ignore = [] # dirs and files to ignore
        @folds = []
        @check_exec = nil
        generate_ignore
      end
    
      ##
      # Retrieve a file's status from +@files+. If it's not there
      # then return :untracked
      # 
      # @param [String] key the path of the file
      # @return [Symbol] status of the file, either :removed, :added, :untracked,
      #   :merged, :normal, :forgotten, or :untracked
      def [](key)
        lookup = @files[key]
        lookup || DirStateEntry.new(:untracked)
      end
    
      ##
      # Determine if +path+ is a link or an executable.
      # 
      # @param [String] path the path to the file
      # @return [String] either 'l' for a link and 'x' for an executable. Returns
      #   '' if neither
      def flags(path)
        return 'l' if File.ftype(path) == 'link'
        return 'x' if File.executable? path
        ''
      end
    
      ##
      # just a lil' reader to find if the repo is dirty or not
      # by dirty i mean "no longer in sync with the cache"
      # 
      # @return [Boolean] is the dirstate no longer in sync with the cache located
      #   at .hg/branch.cache
      def dirty?
        @dirty
      end
    
      ##
      # The directories and path matches that we're ignoringzorz. It will call
      # the ignorer generated by .hgignore, but only if @ignore_all is nil (really
      # only if @ignore_all isn't a Boolean value, but we set it to nil)
      # 
      # @param [String] file the path to the file that will be checked by
      #   the .hgignore file
      # @return [Boolean] whether we're ignoring the path or not
      def ignore(file)
        return true  if @ignore_all == true
        return false if @ignore_all == false
        @ignore_matches ||= parse_ignore @root, @ignore
        @ignore_matches.call file
      end
    
      ##
      # Gets the current branch.
      #
      # @return [String] the current branch in the working directory
      def branch
        text      = @opener.read('branch').strip
        @branch ||= text.empty? ? "default" : text
      rescue
        @branch   = "default"
      end
    
      ##
      # Set the branch to +branch+.
      # 
      # @param [#to_s] brnch the branch to switch to
      # @return [String] +brnch+.to_s
      def branch=(brnch)
        @branch = brnch.to_s
      
        @opener.open 'branch', 'w' do |f|
          f.puts brnch.to_s
        end
        @branch
      end
    
      ##
      # Set the parents to +p+
      # 
      # @param [Array<String>] p the parents as binary strings
      # @return [Array<String>] the parents, as will be used by the dirstate
      def parents=(p)
        @parents = if p.is_a? Array
                     p.size == 1 ? p + [NULL_ID] : p[0..1]
                   else
                     [p, NULL_ID]
                   end
      
        @dirty_parents = true
        @dirty         = true
        @parents # return this
      end
      alias_method :parent, :parents
    
      ##
      # Set the file as "to be added".
      # 
      # @param [String] file the path of the file to add
      # @return [Boolean] a success marker
      def add(file)
        add_path file, true
      
        @dirty = true
        @files[file] = DirStateEntry.new(:added, 0, -1, -1)
        @copy_map.delete file
        true # success
      end
    
      ##
      # Set the file as "normal", meaning no changes. This is the same
      # as dirstate.normal in dirstate.py, for those referencing both.
      # 
      # @param [String] file the path of the file to clean
      # @return [Boolean] a success marker
      def normal(file)
        @dirty = true
        add_path file, true
      
        f = File.lstat "#{@root}/#{file}"
        @files[file] = DirStateEntry.new(:normal, f.mode, f.size, f.mtime.to_i)
        @copy_map.delete file
        true # success
      end
      alias_method :clean, :normal
    
      ##
      # Set the file as normal, but possibly dirty. It's like when you
      # meet a cool girl, and she seems really innocent and it's a chance
      # for you to maybe change yourself and make a new friend, but then
      # she *might* actually be a total slut. Better milk that grapevine
      # to find out the truth. Oddly specific, huh.
      # 
      # THUS IS THE HISTORY OF THIS METHOD!
      #
      # And then one day you go to the movies with some other girl, and the
      # original crazy slutty girl is the cashier next to you. Unsure of
      # what to do, you don't do anything. Next thing you know, she's trying
      # to get your attention to say hey. WTF? Anyone know what's up with this
      # girl?
      # 
      # After milking that grapevine, you find out that she's not a great person.
      # There's nothing interesting there and you should just move on.
      # 
      # *sigh* girls.
      # 
      # @param [String] file the path of the file to mark
      # @return [Boolean] a success marker
      def maybe_dirty(file)
        if @files[file] && @parents.last != NULL_ID
          # if there's a merge happening and the file was either modified
          # or dirty before being removed, restore that state.
          # I'm quoting the python with that one.
          # I guess what it's saying is that if a file is being removed
          # by a merge, but it was altered somehow beforehand on the local
          # repo, then play it safe and bring back the dead. Divine intervention
          # on the side of the local repo.
        
          # info here is a standard array of info
          # [action, mode, size, mtime]
          info = @files[file]
        
          if info.removed? and [-1, -2].member? info.size
            source = @copy_map[file]
          
            # do the appropriate action
            case info.size
            when -1 # either merge it
              merge file
            when -2 # or mark it as dirty
              dirty file
            end
          
            copy source => file if source
            return
          end
        
          # next step... the base case!
          return true if info.modified? || info.maybe_dirty? and info.size == -2
        end
      
        @dirty = true # make the repo dirty
        add_path file # add the file
      
        @files[file] = DirStateEntry.new(:normal, 0, -1, -1) # give it info
        @copy_map.delete file # we're not copying it since we're adding it
        true # success
      end
    
      ##
      # Checks whether the dirstate is tracking the given file.
      # 
      # @param f the file to check for
      # @return [Boolean] whether or not the file is being tracked.
      def include?(path)
        not @files[path].nil?
      end
      alias_method :tracking?, :include?
    
      ##
      # Mark the file as "dirty"
      # 
      # @param [String] file the path of the file to mark
      # @return [Boolean] a success marker
      def dirty(file)
        @dirty = true
        add_path file
      
        @files[file] = DirStateEntry.new(:normal, 0, -2, -1)
        @copy_map.delete file
        true # success
      end
    
      ##
      # Set the file as "to be removed"
      # 
      # @param [String] file the path of the file to remove
      # @return [Boolean] a success marker
      def remove(file)
        @dirty = true
        drop_path file
      
        size = 0
        if @parents.last.null? && (info = @files[file])
          if info.merged?
           size = -1
          elsif info.normal? && info.size == -2
           size = -2
          end
        end
        @files[file] = DirStateEntry.new(:removed, 0, size, 0)
        @copy_map.delete file if size.zero?
        true # success
      end
    
      ##
      # Prepare the file to be merged
      # 
      # @param [String] file the path of the file to merge
      # @return [Boolean] a success marker
      def merge(file)
        @dirty = true
        add_path file
      
        stats = File.lstat "#{@root}/#{file}"
        add_path file
        @files[file] = DirStateEntry.new(:merged, stats.mode, stats.size, stats.mtime.to_i)
        @copy_map.delete file
        true # success
      end
    
      ##
      # Forget the file, erase it from the repo
      # 
      # @param [String] file the path of the file to forget
      # @return [Boolean] a success marker
      def forget(file)
        @dirty = true
        drop_path file
        @files.delete file
        true # success
      end
    
      ##
      # Invalidates the dirstate, making it completely unusable until it is
      # re-read. Should only be used in error situations.
      def invalidate!
        %w(@files @copy_map @folds @branch @parents @dirs @ignore).each do |ivar|
          instance_variable_set(ivar, nil)
        end
        @dirty = false
      end
    
      ##
      # Refresh the directory's state, making everything empty.
      # Called by #rebuild.
      # 
      # This is not the same as #initialize, so we can't just run
      # `send :initialize` and call it a day :-(
      # 
      # @return [Boolean] a success marker
      def clear
        @files    = {}
        @dirs     = {}
        @copy_map = {}
        @parents  = [NULL_ID, NULL_ID]
        @dirty    = true
      
        true # success
      end
    
      ##
      # Rebuild the directory's state. Needs Manifest, as that's
      # what the files really are.
      # 
      # @param [String] parent the binary format of the parent
      # @param [ManifestEntry] files the files in a specific revision
      # @return [Boolean] a success marker
      def rebuild(parent, files)
        clear
      
        # alter each file according to its flags
        files.each do |f|
          mode = files.flags(f).include?('x') ? 0777 : 0666
          @files[f] = DirStateEntry.new(:normal, mode, -1, 0)
        end
      
        @parents = [parent, NULL_ID]
        @dirty_parents = true
        true # success
      end
    
      ##
      # Save the data to .hg/dirstate.
      # Uses mode: "w", so it overwrites everything
      # 
      # @todo watch memory usage - +si+ could grow unrestrictedly which would
      #   bog down the entire program
      # @return [Boolean] a success marker
      def write
        return true unless @dirty
        begin
          @opener.open "dirstate", 'w' do |state|
            gran = @config['dirstate']['granularity'] || 1 # self._ui.config('dirstate', 'granularity', 1)
          
            limit = 2147483647 # sorry for the literal use...
            limit = state.mtime - gran if gran > 0
          
            si = StringIO.new "", (ruby_19? ? "w+:ASCII-8BIT" : "w+")
            si.write @parents.join
          
            @files.each do |file, info|
              file = file.dup # so we don't corrupt vars
              info = info.dup.to_a # UNLIKE PYTHON
              info[0]   = info[0].to_hg_int
            
              # I should probably do mah physics hw. nah, i'll do it
              # tomorrow during my break
              # good news - i did pretty well on my physics test by using
              # brian ford's name instead of my own.
              file = "#{file}\0#{@copy_map[file]}" if @copy_map[file]
              info = [info[0], 0, (-1).to_signed(32), (-1).to_signed(32)] if info[3].to_i > limit.to_i and info[0] == :normal
              info << file.size # the final element to make it pass, which is the length of the filename
              info = info.pack FORMAT # pack them their lunch
              si.write info # and send them off
              si.write file # to school
            end
          
            state.write si.string
            @dirty         = false
            @dirty_parents = false
          
            true # success
          end
        rescue IOError
          false
        end
      end
    
      ##
      # Copies the files in h (represented as "source" => "dest").
      #
      # @param [Hash<String => String>] h the keys are sources and the values 
      #   are dests
      # @return [Boolean] a success marker
      def copy(h={})
        h.each do |source, dest|
          next if source == dest
          return true unless source
        
          @dirty = true
        
          if   @copy_map[dest]
          then @copy_map.delete dest
          else @copy_map[dest] = source
          end
        end
      
        true # success
      end
    
      ##
      # The current directory from where the command is being called, with
      # the path shortened if it's within the repo.
      # 
      # @return [String] effectively Dir.pwd
      def cwd
        path = Dir.pwd
        return '' if path == @root
      
        # return a more local path if possible...
        return path[@root.length..-1] if path.start_with? @root
        path # else we're outside the repo
      end
      alias_method :pwd, :cwd
    
      ##
      # Returns the relative path from +src+ to +dest+.
      # 
      # @param [String] src This is a directory! If this is relative,
      #                     it is assumed to be relative to the root.
      # @param [String] dest This MUST be within root! It also is a file.
      # @return [String] the relative path
      def path_to(src, dest)
        # first, make both paths absolute, for ease of use.
        # @root is guarenteed to be absolute, so we're leethax here
        src  = File.join @root, src
        dest = File.join @root, dest
      
        # lil' bit of error checking...
        [src, dest].map do |f|
          unless File.exist? f # does both files and directories...
            raise FileNotInRootError, "#{f} is not in the root, #{@root}"
          end
        end
      
        # now we find the differences
        # these both are now arrays!!!
        src  = src.split '/'
        dest = dest.split '/' 
      
        while src.first == dest.first
          src.shift and dest.shift
        end
      
        # now, src and dest are just where they differ
        path = ['..'] * src.size # we want to go back this many directories
        path += dest
        path.join '/' # tadah!
      end
    
      ##
      # Walk recursively through the directory tree, finding all
      # files matched by the regexp in match.
      # 
      # Step 1: find all explicit files
      # Step 2: visit subdirectories
      # Step 3: report unseen items in the @files hash
      # 
      # @param [Boolean] unknown
      # @param [Boolean] ignored
      # @return [Hash<String => [NilClass, File::Stat]>] nil for directories and
      #   stuff, File::Stat for files and links
      def walk(unknown, ignored, match)
        files = match.files
      
        bad_type = proc do |file|
          UI::warn "#{file}: unsupported file type (type is #{File.ftype file})"
        end
      
        if ignored
          @ignore_all = false
        elsif not unknown
          @ignore_all = true
        end
      
        work = [@root]
      
        files = match.files ? match.files.uniq : [] # because [].uniq! is a major fuckup
      
        # why do we overwrite the entire array if it includes the current dir?
        # we even kill posisbly good things
        files = [''] if files.empty? || files.include?('.') # strange thing to do
        results = {'.hg' => true}
      
        # Step 1: find all explicit files
        files.sort.each do |file|
          next if results[file] || file == ""
        
          begin
            stats = File.lstat File.join(@root, file)
            kind  = File.ftype File.join(@root, file)
          
            # we'll take it! but only if it's a directory, which means we have
            # more work to do...
            if kind == 'directory'
              # add it to the list of dirs we have to search in
              work << File.join(@root, file) unless ignoring_directory? file
            elsif kind == 'file' || kind == 'link'
              # ARGH WE FOUND ZE BOOTY
              results[file] = stats
            else
              # user you are a fuckup in life please exit the world
              bad_type[file]
              results[file] = nil if @files[file]
            end
          rescue => e
            keep = false
            prefix = file + '/'
          
            @files.each do |f, _|
              if f == file || f.start_with?(prefix)
                keep = true
                break
              end
            end
          
            unless keep
              bad_type[file]
              results[file] = nil if (@files[file] || !ignore(file)) && match.call(file)
            end
          end
        end
      
        # step 2: visit subdirectories in `work`
        until work.empty?
          dir = work.shift
          skip = nil
        
          if dir == '.'
            dir = ''
          else
            skip = '.hg'
          end
        
        
          dirs = Dir.glob("#{dir}/*", File::FNM_DOTMATCH) - ["#{dir}/.", "#{dir}/.."]
          entries = dirs.inject({}) do |h, f|
            h.merge f => [File.ftype(f), File.lstat(f)]
          end
        
        
          entries.each do |f, arr|
            tf = f[(@root.size+1)..-1]
            kind = arr[0]
            stats = arr[1]
            unless results[tf]
              if kind == 'directory'
                work << f unless  ignore tf
                results[tf] = nil if @files[tf] && match.call(tf)
              elsif kind == 'file' || kind == 'link'
                if @files[tf]
                  results[tf] = stats if match.call tf
                elsif match.call(tf) && !ignore(tf)
                  results[tf] = stats
                end
              elsif @files[tf] && match.call(tf)
                results[tf] = nil
              end
            end
          end
        end
      
        # step 3: report unseen items in @files
        visit = @files.keys.select {|f| !results[f] && match.call(f) }.sort
      
        # zip it to a hash of {file_name => file_stats}
        hash  = visit.inject({}) do |h, f|
          h.merge!(f => File.stat(File.join(@root,f))) rescue h.merge!(f => nil)
        end
      
        hash.each do |file, stat|
          unless stat.nil?
            # because filestats can't be gathered if it's, say, a directory
            stat = nil unless ['file', 'link'].include? File.ftype(File.join(@root, file))
          end
          results[file] = stat
        end
      
        results.delete ".hg"
        @ignore_all = nil # reset this
        results
      end
    
      ##
      # what's the current state of life, man!
      # Splits up all the files into modified, clean,
      # added, deleted, unknown, ignored, or lookup-needed.
      # 
      # @return [Hash<Symbol => Array<String>>] a hash of the filestatuses and their files
      def status(ignored, clean, unknown, match = Match.new { true })
        list_ignored, list_clean, list_unknown = ignored, clean, unknown
        lookup, modified, added, unknown, ignored = [], [], [], [], []
        removed, deleted, clean = [], [], []
        delta = 0
      
        walk(list_unknown, list_ignored, match).each do |file, st|
          next if file.nil?
        
          unless @files[file]
            if list_ignored && ignoring_directory?(file)
              ignored << file
            elsif list_unknown
              unknown << file unless ignore(file)
            end
          
            next # on to the next one, don't do the rest
          end
        
          # here's where we split up the files
          state, mode, size, time = *@files[file].to_a
          delta += (size - st.size).abs if st && size >= 0 # increase the delta, but don't forget to check that it's not nil
          if !st && [:normal, :modified, :added].include?(state)
            # add it to the deleted folder if it should be here but isn't
            deleted << file
          elsif state == :normal
            if (size >= 0 && (size != st.size || ((mode ^ st.mode) & 0100 and @check_exec))) || size == -2 || @copy_map[file]
              modified << file
            elsif time != st.mtime.to_i # DOH - we have to remember that times are stored as fixnums
              lookup << file
            elsif list_clean
              clean << file
            end
          
          elsif state == :merged
            modified << file
          elsif state == :added
            added << file
          elsif state == :removed
            removed << file
          end
        end
      
        r = { :modified => modified.sort , # those that have clearly been modified
              :added    => added.sort    , # those that are marked for adding
              :removed  => removed.sort  , # those that are marked for removal
              :deleted  => deleted.sort  , # those that should be here but aren't
              :unknown  => unknown.sort  , # those that aren't being tracked
              :ignored  => ignored.sort  , # those that are being deliberately ignored
              :clean    => clean.sort    , # those that haven't changed
              :lookup   => lookup.sort   , # those that need to be content-checked to see if they've changed
              :delta    => delta           # how many bytes have been added or removed from files (not bytes that have been changed)
            }
      end
    
    
      ##
      # Reads the data in the .hg folder and fills in the vars
      # 
      # @return [Amp::DirState] self -- chainable!
      def read!
        @parents, @files, @copy_map = parse('dirstate')
        self # chainable
      end
    
      private
      ##
      # Generates the @ignore array
      # The array is full of paths relative to the root, which
      # makes things easier for the proc-generation phase.
      # 
      # @return [NilClass]
      def generate_ignore
        @ignore = @config['ui'].map do |k, v|
          @ignore << "#{v}" if k == "ignore"
        end
      
        @ignore << ".hgignore"
        @ignore.compact
      
        nil
      end
    
      ##
      # Perform various checks on the file before upping the content count
      # for all of its parent directories. It checks for:
      #   * filenames containing "\n" or "\r" (newlines and carriage returns)
      #   * filenames with the same names as directories
      #   * clashing filenames
      # 
      # It only increments the dirs' file count if the file is untracked or
      # being removed.
      # 
      # @param [String] f Should be formatted like ["action", mode, size, mtime]
      # @param [Boolean] check whether to perform any of the checks
      # @return [NilClass]
      def add_path(f, check=false)
        old_state = @files[f] || DirStateEntry.new # it's an array of info, remember
      
        if check || old_state.removed?
          raise "Bad Filename" if f.match(/\r|\n/)
          raise "Directory #{f} already exists" if @dirs[f]
        
          # make sure we don't have any files with the same name as a directory
          directories_to(f).each do |d|
            break if @dirs[d]
          
            if @files[d] && !@files[d].removed?
              raise "File #{d} clashes with #{f}! Fix their names" 
            end
          end
        end
      
        # only inc the dirs if the file is untracked or being removed.
        if [:untracked, :removed].include? old_state.status
          # inc the number of dirs in each dir
          inc_directories_to f
        end
      
        nil
      end
    
      ##
      # Conditional wrapper around +dec_directories_to+. It will dec the
      # directories if the file in question (+f+) is either untracked or
      # being removed.
      # 
      # @param [String] f Should be formatted like ["action", mode, size, mtime]
      # @return [NilClass]
      def drop_path(f)
        unless [:untracked, :removed].include? f[0]
          dec_directories_to(f)
        end
      
        nil
      end
    
      ##
      # All directories leading up to this path
      # 
      # @example directories_to "/Users/ari/src/monkey.txt" # => 
      #                           ["/Users/ari/src", "/Users/ari", "/Users"]
      # @param [String] path the path to the file we're examining
      # @return [Array] the directories leading up to this path
      def directories_to(path)
        File.amp_directories_to path
      end
    
      ##
      # Increment all directories' dir-count leading up to this path.
      # The dir-count is the path's value in @dirs.
      # This is used when adding a file.
      # 
      # @param [String] path the path we're disecting
      # @return [NilClass]
      def inc_directories_to(path)
        p = directories_to(path).first
        @dirs[p] ||= 0
        @dirs[p] += 1
        nil
      end
    
      ##
      # Decrement all directories' dir-count leading up to this path.
      # The dir-count is the path's value in @dirs.
      # This is used when removing a file.
      # 
      # @param [String] path the path we're disecting
      # @return [NilClass]
      def dec_directories_to(path)
        p = directories_to(path).first
        # if the dir has 0, kill the dir. we don't need it anymore
        if @dirs[p] && @dirs[p].zero?
          @dirs.delete p
        elsif @dirs[p]
          @dirs[p] -= 1 # we only need to inc the latest dir
        end
      
        nil
      end
    
      ##
      # I wish I knew what this did or when it was called.
      # 
      # @todo figure out what this does
      # @param [String] path the path to a file
      # @return [String] All I know is that this returns a string
      def normalize(path)
        fold_path = @folds[path]
        fold_path = path unless fold_path # if fold_path is true, then this line returns nil
        fold_path # so we need an extra line here to make sure it returns a good value
      end
    
      ##
      # Are we ignoring the directory?
      # 
      # @param [String] dir the directory we're checking, either aboslute or relative
      # @return [Boolean] are we ignoring the dir?
      def ignoring_directory?(dir)
        return true  if @ignore_all
        return false if @ignore_all == false
        return false if dir == '.'  # base cases
        return true  if ignore dir  # base cases
      
        !!directories_to(dir).any? {|d| ignore d }
      end
      alias_method :ignoring_dir?, :ignoring_directory?
    
      ##
      # Parses the dirstate file in .hg
      # 
      # @param [String] file path to the file to parse
      # @return [((String, String), Hash<String => (Integer, Integer, Integer)>, Hash<String => String>)]
      #   a tuple of (parents, files, copies). Parents is a tuple of the parents,
      #   files is a hash of filename => [mode, size, mtime], and copies is a hash of src => dest
      def parse(file)
        # the main data we need to return
        files  = {}
        copies = {}
        parents = []
        @opener.open file, "r" do |s|
        
          # the parents are the first 40 bytes
          parent  = s.read(20)  || NULL_ID
          parent_ = s.read(20)  || NULL_ID
          parents = [parent, parent_] 
        
          # 1 character + 4 32-bit ints = 17 bytes
          e_size = 17
        
          # this loop is just cycling through and reading every entry
          while !s.eof?
            # read 1 entry
            info = s.read(e_size).unpack FORMAT
          
            # byte swap and shizzle
            info = [info[0].to_dirstate_symbol, info[1], info[2].to_signed(32), info[3].to_signed(32), info[4]]
            # ^^^^ we have to sign them because otherwise they'll be hugely wrong
          
            # read in the filename
            f = s.read(info[4])
              
            # if it has an \0, then we've moved/copied it
            if f.match(/\0/)
              source, dest = f.split "\0"
              copies[source] = dest
              f = source
            end
              
            # and put in the info for the file itself
            files[f] = DirStateEntry.new(*info[0..3])
          end
        end
  
        [parents, files, copies]
      rescue Errno::ENOENT
        # no file? easy peasy
        [[NULL_ID, NULL_ID], {}, {}]
      end
    end
  end
end