Class: Amp::Repositories::DirState

Inherits:
Object
  • Object
show all
Includes:
Ignore, Amp::RevlogSupport::Node
Defined in:
lib/amp/repository/dir_state.rb

Overview

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

Defined Under Namespace

Classes: AbsolutePathNeededError, FileNotInRootError

Constant Summary collapse

UNKNOWN =
DirStateEntry.new(:untracked, 0, 0, 0)
FORMAT =
"cNNNN"

Constants included from Amp::RevlogSupport::Node

Amp::RevlogSupport::Node::NULL_ID, Amp::RevlogSupport::Node::NULL_REV

Constants included from Ignore

Ignore::COMMENT, Ignore::SYNTAXES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Amp::RevlogSupport::Node

#short

Methods included from Ignore

#matcher_for_string, #matcher_for_text, #parse_ignore, #parse_line, #parse_lines, #regexps_to_proc

Constructor Details

#initialize(root, config, opener) ⇒ DirState

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).



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/amp/repository/dir_state.rb', line 101

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

Instance Attribute Details

#configObject (readonly)

The conglomerate config object of global configs and the repo specific config.



83
84
85
# File 'lib/amp/repository/dir_state.rb', line 83

def config
  @config
end

#copy_mapObject (readonly)

A map of files to be copied, because we want to preserve their history “source” => “dest”



76
77
78
# File 'lib/amp/repository/dir_state.rb', line 76

def copy_map
  @copy_map
end

#dirsObject (readonly)

The number of directories in each base [“dir” => #_of_dirs]



68
69
70
# File 'lib/amp/repository/dir_state.rb', line 68

def dirs
  @dirs
end

#filesObject (readonly)

The files mapped to their stats (state, mode, size, mtime)

state, mode, size, mtime


72
73
74
# File 'lib/amp/repository/dir_state.rb', line 72

def files
  @files
end

#foldsObject (readonly)

I still don’t know what this does



79
80
81
# File 'lib/amp/repository/dir_state.rb', line 79

def folds
  @folds
end

#openerObject (readonly)

The opener to access files. The only files that will be touched lie in the .hg/ directory, so the default MUST be :open_hg.



90
91
92
# File 'lib/amp/repository/dir_state.rb', line 90

def opener
  @opener
end

#parentsObject Also known as: parent

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



65
66
67
# File 'lib/amp/repository/dir_state.rb', line 65

def parents
  @parents
end

#rootObject (readonly)

The root of the repository



86
87
88
# File 'lib/amp/repository/dir_state.rb', line 86

def root
  @root
end

Instance Method Details

#[](key) ⇒ Symbol

Retrieve a file’s status from @files. If it’s not there then return :untracked



129
130
131
132
# File 'lib/amp/repository/dir_state.rb', line 129

def [](key)
  lookup = @files[key]
  lookup || DirStateEntry.new(:untracked)
end

#add(file) ⇒ Boolean

Set the file as “to be added”.



219
220
221
222
223
224
225
226
# File 'lib/amp/repository/dir_state.rb', line 219

def add(file)
  add_path file, true

  @dirty = true
  @files[file] = DirStateEntry.new(:added, 0, -1, -1)
  @copy_map.delete file
  true # success
end

#branchString

Gets the current branch.



175
176
177
178
179
180
# File 'lib/amp/repository/dir_state.rb', line 175

def branch
  text      = @opener.read('branch').strip
  @branch ||= text.empty? ? "default" : text
rescue
  @branch   = "default"
end

#branch=(brnch) ⇒ String

Set the branch to branch.



187
188
189
190
191
192
193
194
# File 'lib/amp/repository/dir_state.rb', line 187

def branch=(brnch)
  @branch = brnch.to_s

  @opener.open 'branch', 'w' do |f|
    f.puts brnch.to_s
  end
  @branch
end

#clearBoolean

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 :-(



400
401
402
403
404
405
406
407
408
# File 'lib/amp/repository/dir_state.rb', line 400

def clear
  @files    = {}
  @dirs     = {}
  @copy_map = {}
  @parents  = [NULL_ID, NULL_ID]
  @dirty    = true

  true # success
end

#copy(h = {}) ⇒ Boolean

Copies the files in h (represented as “source” => “dest”).



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/amp/repository/dir_state.rb', line 484

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

#cwdString Also known as: pwd

The current directory from where the command is being called, with the path shortened if it’s within the repo.



505
506
507
508
509
510
511
512
# File 'lib/amp/repository/dir_state.rb', line 505

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

#dirty(file) ⇒ Boolean

Mark the file as “dirty”



323
324
325
326
327
328
329
330
# File 'lib/amp/repository/dir_state.rb', line 323

def dirty(file)
  @dirty = true
  add_path file

  @files[file] = DirStateEntry.new(:normal, 0, -2, -1)
  @copy_map.delete file
  true # success
end

#dirty?Boolean

just a lil’ reader to find if the repo is dirty or not by dirty i mean “no longer in sync with the cache”



152
153
154
# File 'lib/amp/repository/dir_state.rb', line 152

def dirty?
  @dirty
end

#flags(path) ⇒ String

Determine if path is a link or an executable.



140
141
142
143
144
# File 'lib/amp/repository/dir_state.rb', line 140

def flags(path)
  return 'l' if File.ftype(path) == 'link'
  return 'x' if File.executable? path
  ''
end

#forget(file) ⇒ Boolean

Forget the file, erase it from the repo



375
376
377
378
379
380
# File 'lib/amp/repository/dir_state.rb', line 375

def forget(file)
  @dirty = true
  drop_path file
  @files.delete file
  true # success
end

#ignore(file) ⇒ Boolean

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)



164
165
166
167
168
169
# File 'lib/amp/repository/dir_state.rb', line 164

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

#include?(path) ⇒ Boolean Also known as: tracking?

Checks whether the dirstate is tracking the given file.



313
314
315
# File 'lib/amp/repository/dir_state.rb', line 313

def include?(path)
  not @files[path].nil?
end

#invalidate!Object

Invalidates the dirstate, making it completely unusable until it is re-read. Should only be used in error situations.



385
386
387
388
389
390
# File 'lib/amp/repository/dir_state.rb', line 385

def invalidate!
  %w(@files @copy_map @folds @branch @parents @dirs @ignore).each do |ivar|
    instance_variable_set(ivar, nil)
  end
  @dirty = false
end

#maybe_dirty(file) ⇒ Boolean

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.



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/amp/repository/dir_state.rb', line 267

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

#merge(file) ⇒ Boolean

Prepare the file to be merged



359
360
361
362
363
364
365
366
367
368
# File 'lib/amp/repository/dir_state.rb', line 359

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

#normal(file) ⇒ Boolean Also known as: clean

Set the file as “normal”, meaning no changes. This is the same as dirstate.normal in dirstate.py, for those referencing both.



234
235
236
237
238
239
240
241
242
# File 'lib/amp/repository/dir_state.rb', line 234

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

#path_to(src, dest) ⇒ String

Returns the relative path from src to dest.



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
# File 'lib/amp/repository/dir_state.rb', line 522

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

#read!Amp::DirState

Reads the data in the .hg folder and fills in the vars



749
750
751
752
# File 'lib/amp/repository/dir_state.rb', line 749

def read!
  @parents, @files, @copy_map = parse('dirstate')
  self # chainable
end

#rebuild(parent, files) ⇒ Boolean

Rebuild the directory’s state. Needs Manifest, as that’s what the files really are.



417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/amp/repository/dir_state.rb', line 417

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

#remove(file) ⇒ Boolean

Set the file as “to be removed”



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/amp/repository/dir_state.rb', line 337

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

#status(ignored, clean, unknown, match = Match.new { true }) ⇒ Hash<Symbol => Array<String>>

what’s the current state of life, man! Splits up all the files into modified, clean, added, deleted, unknown, ignored, or lookup-needed.



689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'lib/amp/repository/dir_state.rb', line 689

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

#walk(unknown, ignored, match) ⇒ Hash<String => [NilClass, File::Stat]>

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



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# File 'lib/amp/repository/dir_state.rb', line 562

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

#writeBoolean

TODO:

watch memory usage - si could grow unrestrictedly which would bog down the entire program

Save the data to .hg/dirstate. Uses mode: “w”, so it overwrites everything



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/amp/repository/dir_state.rb', line 438

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