Class: Amp::Repositories::DirState
- 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
: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
-
#config ⇒ Object
readonly
The conglomerate config object of global configs and the repo specific config.
-
#copy_map ⇒ Object
readonly
A map of files to be copied, because we want to preserve their history “source” => “dest”.
-
#dirs ⇒ Object
readonly
The number of directories in each base [“dir” => #_of_dirs].
-
#files ⇒ Object
readonly
The files mapped to their stats (state, mode, size, mtime) [state, mode, size, mtime].
-
#folds ⇒ Object
readonly
I still don’t know what this does.
-
#opener ⇒ Object
readonly
The opener to access files.
-
#parents ⇒ Object
(also: #parent)
The parents of the current state.
-
#root ⇒ Object
readonly
The root of the repository.
Instance Method Summary collapse
-
#[](key) ⇒ Symbol
Retrieve a file’s status from @files.
-
#add(file) ⇒ Boolean
Set the file as “to be added”.
-
#branch ⇒ String
Gets the current branch.
-
#branch=(brnch) ⇒ String
Set the branch to
branch. -
#clear ⇒ Boolean
Refresh the directory’s state, making everything empty.
-
#copy(h = {}) ⇒ Boolean
Copies the files in h (represented as “source” => “dest”).
-
#cwd ⇒ String
(also: #pwd)
The current directory from where the command is being called, with the path shortened if it’s within the repo.
-
#dirty(file) ⇒ Boolean
Mark the file as “dirty”.
-
#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”.
-
#flags(path) ⇒ String
Determine if
pathis a link or an executable. -
#forget(file) ⇒ Boolean
Forget the file, erase it from the repo.
-
#ignore(file) ⇒ Boolean
The directories and path matches that we’re ignoringzorz.
-
#include?(path) ⇒ Boolean
(also: #tracking?)
Checks whether the dirstate is tracking the given file.
-
#initialize(root, config, opener) ⇒ DirState
constructor
Creates a DirState object.
-
#invalidate! ⇒ Object
Invalidates the dirstate, making it completely unusable until it is re-read.
-
#maybe_dirty(file) ⇒ Boolean
Set the file as normal, but possibly dirty.
-
#merge(file) ⇒ Boolean
Prepare the file to be merged.
-
#normal(file) ⇒ Boolean
(also: #clean)
Set the file as “normal”, meaning no changes.
-
#path_to(src, dest) ⇒ String
Returns the relative path from
srctodest. -
#read! ⇒ Amp::DirState
Reads the data in the .hg folder and fills in the vars.
-
#rebuild(parent, files) ⇒ Boolean
Rebuild the directory’s state.
-
#remove(file) ⇒ Boolean
Set the file as “to be removed”.
-
#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.
-
#walk(unknown, ignored, match) ⇒ Hash<String => [NilClass, File::Stat]>
Walk recursively through the directory tree, finding all files matched by the regexp in match.
-
#write ⇒ Boolean
Save the data to .hg/dirstate.
Methods included from Amp::RevlogSupport::Node
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
#config ⇒ Object (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_map ⇒ Object (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 |
#dirs ⇒ Object (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 |
#files ⇒ Object (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 |
#folds ⇒ Object (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 |
#opener ⇒ Object (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 |
#parents ⇒ Object 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 |
#root ⇒ Object (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 |
#branch ⇒ String
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 |
#clear ⇒ Boolean
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 |
#cwd ⇒ String 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 |
#write ⇒ Boolean
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 |