├── var ├── title ├── name ├── created ├── version ├── organization ├── copyrights ├── authors ├── requirements ├── repositories ├── summary ├── resources └── description ├── lib ├── mast.yml ├── mast.rb └── mast │ ├── core_ext.rb │ ├── cli.rb │ └── manifest.rb ├── work ├── trash │ ├── meta │ │ ├── name │ │ ├── version │ │ ├── suite │ │ ├── sitemap │ │ ├── contact │ │ ├── ruby │ │ ├── homepage │ │ ├── wiki │ │ ├── authors │ │ ├── repository │ │ ├── download │ │ ├── summary │ │ └── description │ ├── lister.rb │ ├── listing.rb │ ├── generator.rb │ └── manifest2.rb └── lr │ └── bin │ └── lr ├── Gemfile ├── man └── man1 │ ├── index.txt │ ├── mast.1.ronn │ └── mast.1 ├── bin └── mast ├── .gitignore ├── .travis.yml ├── MANIFEST ├── demo ├── functional │ ├── applique │ │ └── rules.rb │ └── 02_manifest.rdoc └── cli │ ├── applique │ └── env.rb │ └── overview.rdoc ├── Assembly ├── .ruby ├── COPYING.rdoc ├── HISTORY.rdoc ├── README.rdoc ├── DEMO.rdoc └── .gemspec /var/title: -------------------------------------------------------------------------------- 1 | Mast -------------------------------------------------------------------------------- /var/name: -------------------------------------------------------------------------------- 1 | mast 2 | -------------------------------------------------------------------------------- /lib/mast.yml: -------------------------------------------------------------------------------- 1 | ../.ruby -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2009-08-17 -------------------------------------------------------------------------------- /var/version: -------------------------------------------------------------------------------- 1 | 1.4.0 2 | -------------------------------------------------------------------------------- /var/organization: -------------------------------------------------------------------------------- 1 | rubyworks 2 | -------------------------------------------------------------------------------- /work/trash/meta/name: -------------------------------------------------------------------------------- 1 | mast 2 | -------------------------------------------------------------------------------- /work/trash/meta/version: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /work/trash/meta/suite: -------------------------------------------------------------------------------- 1 | proutils 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /work/trash/meta/sitemap: -------------------------------------------------------------------------------- 1 | doc/rdoc: mast 2 | -------------------------------------------------------------------------------- /man/man1/index.txt: -------------------------------------------------------------------------------- 1 | mast(1) mast.1.html 2 | -------------------------------------------------------------------------------- /var/copyrights: -------------------------------------------------------------------------------- 1 | --- 2 | - (c) 2009 Thomas Sawyer 3 | -------------------------------------------------------------------------------- /work/trash/meta/contact: -------------------------------------------------------------------------------- 1 | proutils@googlegroups.com 2 | -------------------------------------------------------------------------------- /work/trash/meta/ruby: -------------------------------------------------------------------------------- 1 | --- 2 | - 1.8.6 3 | - 1.8.7 4 | -------------------------------------------------------------------------------- /var/authors: -------------------------------------------------------------------------------- 1 | --- 2 | - Trans 3 | 4 | -------------------------------------------------------------------------------- /work/trash/meta/homepage: -------------------------------------------------------------------------------- 1 | http://proutils.github.com/mast 2 | -------------------------------------------------------------------------------- /work/trash/meta/wiki: -------------------------------------------------------------------------------- 1 | http://wiki.github.com/proutils/mast/ 2 | -------------------------------------------------------------------------------- /work/trash/meta/authors: -------------------------------------------------------------------------------- 1 | - Thomas Sawyer 2 | -------------------------------------------------------------------------------- /work/trash/meta/repository: -------------------------------------------------------------------------------- 1 | git://github.com/proutils/mast.git 2 | -------------------------------------------------------------------------------- /var/requirements: -------------------------------------------------------------------------------- 1 | --- 2 | - detroit (build) 3 | - qed (test) 4 | 5 | -------------------------------------------------------------------------------- /work/trash/meta/download: -------------------------------------------------------------------------------- 1 | http://github.com/proutils/mast/downloads 2 | -------------------------------------------------------------------------------- /bin/mast: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'mast/cli' 3 | Mast::Cli.run 4 | 5 | -------------------------------------------------------------------------------- /var/repositories: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git://github.com/rubyworks/mast.git 3 | -------------------------------------------------------------------------------- /var/summary: -------------------------------------------------------------------------------- 1 | Mast is a command line tool for generating manifests and digests. -------------------------------------------------------------------------------- /work/trash/meta/summary: -------------------------------------------------------------------------------- 1 | Mast is a command line tool for generating manifests and digests. 2 | -------------------------------------------------------------------------------- /work/lr/bin/lr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | Dir['**/*'].sort.each do |path| 4 | puts path 5 | end 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | doc 4 | log 5 | man/man1/*.1 6 | man/man1/*.html 7 | pkg 8 | tmp 9 | web 10 | work/sandbox 11 | DEMO.* 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed qed/functional/" 3 | rvm: 4 | - 1.9.3 5 | - 1.9.2 6 | - 1.8.7 7 | - ree 8 | #- jruby 9 | - rbx-2.0 10 | 11 | -------------------------------------------------------------------------------- /var/resources: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/mast/ 3 | code: http://github.com/rubyworks/mast/ 4 | mail: http://groups.google.com/group/rubyworks-mailinglist 5 | 6 | -------------------------------------------------------------------------------- /var/description: -------------------------------------------------------------------------------- 1 | Mast is a command line tool for generating manifests and digests. Mast makes 2 | it easy to compare a manifest to a current directory structure, and to update 3 | the manifest with a simple command by storing the command options it the 4 | manifest file itself. 5 | -------------------------------------------------------------------------------- /work/trash/meta/description: -------------------------------------------------------------------------------- 1 | Mast is a command line tool for generating manifests and digests. 2 | Mast makes it easy to compare a manifest to a current directory structure, 3 | and to update the manifest with a simple command by storing the command 4 | option it the manifest file itself. 5 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .ruby .yardopts bin demo lib man spec test [A-Z]*.* 2 | .ruby 3 | bin/mast 4 | demo/cli/applique/env.rb 5 | demo/cli/overview.rdoc 6 | demo/functional/02_manifest.rdoc 7 | demo/functional/applique/rules.rb 8 | lib/mast/cli.rb 9 | lib/mast/core_ext.rb 10 | lib/mast/manifest.rb 11 | lib/mast.rb 12 | lib/mast.yml 13 | man/man1/index.txt 14 | man/man1/mast.1 15 | man/man1/mast.1.html 16 | man/man1/mast.1.ronn 17 | HISTORY.rdoc 18 | DEMO.rdoc 19 | README.rdoc 20 | COPYING.rdoc 21 | -------------------------------------------------------------------------------- /lib/mast.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | # Access to project metadata. 3 | def self.metadata 4 | @metadata ||= ( 5 | require 'yaml' 6 | YAML.load(File.new(File.dirname(__FILE__) + '/mast.yml')) 7 | ) 8 | end 9 | 10 | # Access project metadata via constants. 11 | def self.const_missing(name) 12 | key = name.to_s.downcase 13 | package[key] || super(name) 14 | end 15 | 16 | # becuase Ruby 1.8~ gets in the way 17 | VERSION = metadata['version'] 18 | end 19 | 20 | require 'mast/manifest' 21 | -------------------------------------------------------------------------------- /demo/functional/applique/rules.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'mast' 3 | 4 | Before :demo do 5 | if /tmp\/qed$/ =~ Dir.pwd 6 | Dir['*'].each do |file| 7 | FileUtils.rm_r(file) 8 | end 9 | end 10 | end 11 | 12 | When "Lets say we have a directory containing a set of files as follows" do |text| 13 | text.lines.each do |line| 14 | file = line.strip 15 | FileUtils.mkdir_p(File.dirname(file)) 16 | File.open(file, 'w') do |f| 17 | f << file 18 | end 19 | end 20 | end 21 | 22 | 23 | -------------------------------------------------------------------------------- /work/trash/lister.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | 3 | # Lister simple opens a current MANIFEST file and dumps 4 | # it's contents to standard out. 5 | # 6 | class Lister 7 | DEFAULT = 'MANIFEST{,.txt}' 8 | 9 | attr_accessor :file 10 | 11 | # Generate manifest listing. 12 | 13 | def list 14 | glob = file || DEFAULT 15 | file = Dir[glob].first 16 | raise "no manifest" unless file 17 | File.open(file, 'r') do |f| 18 | $stdout << f.read 19 | end 20 | end 21 | end 22 | 23 | end 24 | 25 | -------------------------------------------------------------------------------- /demo/functional/02_manifest.rdoc: -------------------------------------------------------------------------------- 1 | == Manifest Generation 2 | 3 | Lets say we have a directory containing a set of files as follows: 4 | 5 | README.txt 6 | lib/foo.rb 7 | lib/bar.rb 8 | 9 | We can crate a new Manifest object, utilizing a StringIO object to catch 10 | the output. 11 | 12 | out = '' 13 | 14 | manifest = Mast::Manifest.new(:io=>StringIO.new(out)) 15 | 16 | If we call the generate method, then the output should list the 17 | above file along with a generic shebang header. 18 | 19 | manifest.generate 20 | 21 | list = out.split("\n").sort 22 | 23 | list.assert == ['#!mast *','README.txt','lib/bar.rb','lib/foo.rb'] 24 | 25 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | gem: 3 | active: true 4 | 5 | github: 6 | gh_pages: web 7 | 8 | dnote: 9 | title: Source Notes 10 | output: log/notes.html 11 | 12 | #locat: 13 | # output : ~ 14 | # active : true 15 | 16 | qed: 17 | files: demo/ 18 | 19 | qedoc: 20 | files: demo/ 21 | title: Mast Demonstration 22 | output: DEMO.rdoc 23 | 24 | # todo: use ronn service when available 25 | ronn: 26 | service : custom 27 | cycle : main 28 | document : | 29 | system 'ronn --manual="Mast" --organization="RubyWorks" --style="toc" man/man1/*.ronn' 30 | 31 | vclog: 32 | output: 33 | - log/history.html 34 | - log/changes.html 35 | 36 | email: 37 | mailto: 38 | - ruby-talk@ruby-lang.org 39 | - rubyworks-mailinglist@googlegroups.com 40 | 41 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | source: 3 | - var 4 | authors: 5 | - name: Trans 6 | email: transfire@gmail.com 7 | copyrights: 8 | - holder: Thomas Sawyer 9 | year: '2009' 10 | replacements: [] 11 | alternatives: [] 12 | requirements: 13 | - name: detroit 14 | groups: 15 | - build 16 | development: true 17 | - name: qed 18 | groups: 19 | - test 20 | development: true 21 | dependencies: [] 22 | conflicts: [] 23 | repositories: 24 | - uri: git://github.com/rubyworks/mast.git 25 | scm: git 26 | name: upstream 27 | resources: 28 | home: http://rubyworks.github.com/mast/ 29 | code: http://github.com/rubyworks/mast/ 30 | mail: http://groups.google.com/group/rubyworks-mailinglist 31 | extra: {} 32 | load_path: 33 | - lib 34 | revision: 0 35 | created: '2009-08-17' 36 | summary: Mast is a command line tool for generating manifests and digests. 37 | title: Mast 38 | version: 1.4.0 39 | name: mast 40 | description: ! 'Mast is a command line tool for generating manifests and digests. 41 | Mast makes 42 | 43 | it easy to compare a manifest to a current directory structure, and to update 44 | 45 | the manifest with a simple command by storing the command options it the 46 | 47 | manifest file itself.' 48 | organization: rubyworks 49 | date: '2011-11-11' 50 | -------------------------------------------------------------------------------- /demo/cli/applique/env.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'open3' 3 | 4 | def assert_bash(text) 5 | lines = text.lines.to_a 6 | bash = lines[0] 7 | result = lines[1..-1].join.strip 8 | 9 | bash.sub!('$', '') 10 | 11 | stdin, stdout, stderr = Open3.popen3(bash) 12 | 13 | #output = (stderr.read + stdout.read).strip 14 | output = stdout.read.strip 15 | 16 | result = result.split("\n").sort 17 | output = output.split("\n").sort 18 | 19 | result.assert == output 20 | end 21 | 22 | Before :demo do 23 | if /tmp\/qed$/ =~ Dir.pwd 24 | Dir['*'].each do |file| 25 | FileUtils.rm_r(file) 26 | end 27 | end 28 | end 29 | 30 | When "Lets say we have a directory containing a set of files as follows" do |text| 31 | text.lines.each do |line| 32 | file = line.strip 33 | FileUtils.mkdir_p(File.dirname(file)) 34 | File.open(file, 'w') do |f| 35 | f << file 36 | end 37 | end 38 | end 39 | 40 | When "let's say we have a new file" do |text| 41 | text.lines.each do |line| 42 | file = line.strip 43 | FileUtils.mkdir_p(File.dirname(file)) 44 | File.open(file, 'w') do |f| 45 | f << file 46 | end 47 | end 48 | end 49 | 50 | When "let's remove a file" do |text| 51 | text.lines.each do |line| 52 | file = line.strip 53 | FileUtils.rm(file) 54 | end 55 | end 56 | 57 | When :data do |step| 58 | text = step.sample_text 59 | if text.start_with?('$') 60 | assert_bash(text) 61 | end 62 | end 63 | 64 | 65 | -------------------------------------------------------------------------------- /COPYING.rdoc: -------------------------------------------------------------------------------- 1 | = COPYRIGHT NOTICES 2 | 3 | == Mast 4 | 5 | Copyright:: (c) 2009 Rubyworks 6 | License:: BSD-2-Clause 7 | Website:: http://rubyworks.github.com/tapout 8 | 9 | Copyright 2009 Rubyworks. All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | 1. Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | 17 | 2. Redistributions in binary form must reproduce the above copyright 18 | notice, this list of conditions and the following disclaimer in the 19 | documentation and/or other materials provided with the distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 22 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 23 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 26 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 28 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 30 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | -------------------------------------------------------------------------------- /HISTORY.rdoc: -------------------------------------------------------------------------------- 1 | = RELEASE HISTORY 2 | 3 | == 1.3.0 / 2011-10-30 4 | 5 | This release adds a few additional features, see changes list below. It also 6 | changes the license to BSD-2-Clause and modernizes the build configuration. 7 | 8 | Changes: 9 | 10 | * Add `--recent` command to check if up to date. 11 | * Add `--no-head` option to suppress header. 12 | * Improve help system with man-page. 13 | * Update output shows diff if file changed. 14 | * Modernize build configuration. 15 | * Switch to BSD-2-Cluase license. 16 | 17 | 18 | == 1.3.0 / 2010-11-21 19 | 20 | Version 1.3 primarily makes a few adjustment under-the-hood. It switches 21 | GetoptLong out for OptionParser, gets rid of it's dependency on Ruby Facets' 22 | Kernel#ask method, and changes the diff comment short name from `-d` to `-D`. 23 | Lowecase `-d` is now used as a short name for the `--dir` option. In the 24 | process a bug was fixed where this `-d` option was not being added to the 25 | manifest's bang line. Lastly, the `-s`/`--show` option has been renamed to 26 | `-b`/`--bang`, which is more descriptive. 27 | 28 | Changes: 29 | 30 | * Use `optparse.rb` library instead of `getoptlong.rb`. 31 | * Remove dependency on Ruby Facets's Kernel#ask method. 32 | * Rename `-d` to `-D`, short for `--diff`. 33 | * Add `-d` option to go with `--dir`. 34 | * Fix issue with `-d` option not added to bang line. 35 | * Rename `-s`/`--show` option to `-b`/`--bang`. 36 | 37 | 38 | == 1.2.0 / 2010-02-19 39 | 40 | This release adjusts the plugins directory to conform 41 | to the new Plugin gem and add the -i shortcut to the 42 | command-line help. 43 | 44 | Changes: 45 | 46 | * Rename lib/plugin to lib/plugins. 47 | * Add -i to help output. 48 | 49 | 50 | == 1.1.0 / 2009-10-24 51 | 52 | This release fixes the show and clean commands and makes 53 | some additional adjustments under the hood. 54 | 55 | Changes: 56 | 57 | * Fix clean and show commands. 58 | * Change to MIT license. 59 | 60 | 61 | == 1.0.0 / 2009-07-02 62 | 63 | This is the initial release of Mast. 64 | 65 | Changes: 66 | 67 | * Happy Birthday! 68 | 69 | -------------------------------------------------------------------------------- /man/man1/mast.1.ronn: -------------------------------------------------------------------------------- 1 | mast(1) - manifest generator 2 | ============================ 3 | 4 | ## SYNOPSIS 5 | 6 | `mast [] [...]` 7 | 8 | ## DESCRIPTION 9 | 10 | The manifest listing tool is used to list, create or update a 11 | manifest for a directory (eg. to define a "package"), or compare 12 | a manifest to actual directory contents. Mast is part of the 13 | ProUtils set of tools. 14 | 15 | When no command is given, a manifest is dumped to standard out. 16 | If --file is specified, it will generate to that file instead. 17 | 18 | 19 | ## COMMANDS 20 | 21 | * `-c`, `--create`: 22 | Generate a new manifest. (default) 23 | 24 | * `-u`, `--update`: 25 | Update an existing manifest. 26 | 27 | * `-l`, `--list`: 28 | List the files given in the manifest file. (Use -f to specify an alternate file.) 29 | 30 | * `-D`, `--diff`: 31 | Diff manifest file against actual. 32 | 33 | * `-n`, `--new`: 34 | List existant files that are not given in the manifest. 35 | 36 | * `-o`, `--old`: 37 | List files given in the manifest but are non-existent. 38 | 39 | * `-v`, `--verify`: 40 | Verify that a manifest matches actual. 41 | 42 | * `-r`, `--recent`: 43 | Verify that a manifest is more recent than actual. 44 | 45 | * `--clean`: 46 | Remove non-manifest files. (Will ask for confirmation first.) 47 | 48 | * `-h`, `--help`: 49 | Display this help message. 50 | 51 | 52 | ## OPTIONS 53 | 54 | * `-a`, `--all`: 55 | Include all files. This deactivates deafult exclusions 56 | so it is possible to make complete list of all contents. 57 | 58 | * `-d`, `--dir`: 59 | When creating a list include directory paths; by default 60 | only files are listed. 61 | 62 | * `-b`, `--bang`: 63 | Generate manifest using the options from the bang line of the manifest file. 64 | 65 | * `-f`, `--file PATH`: 66 | Path to manifest file. This applies to comparison commands. 67 | If not given then the file matching 'MANIFEST', case-insensitive 68 | and with an optional '.txt' extension, in the current directory 69 | is used. If the path of the manifest file is anything else then 70 | the --file option must be specified. 71 | 72 | * `-g`, `--digest TYPE`: 73 | Include crytographic signiture. Type can be either 74 | md5, sha1, sha128, sha256, or sha512. 75 | 76 | * `-x`, `--exclude PATH`: 77 | Exclude a file or dir from the manifest matching against 78 | full pathname. You can use --exclude repeatedly. 79 | 80 | * `-i`, `--ignore PATH`: 81 | Exclude a file or dir from the manifest matching against 82 | an entries basename. You can use --ignore repeatedly. 83 | 84 | * `--no-head`: 85 | Suppress mast header from output. 86 | 87 | * `--debug`: 88 | Run command with Ruby's $DEBUG flag set to `true`. 89 | 90 | 91 | ## EXAMPLES 92 | 93 | `mast`
94 | `mast -u -f PUBLISH` 95 | 96 | 97 | ## SEE ALSO 98 | 99 | ls(1) 100 | -------------------------------------------------------------------------------- /work/trash/listing.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | 3 | # 4 | class Listing 5 | 6 | DEFAULT_EXCLUDE = %w{InstalledFiles .config *~ CVS _darcs .svn .git} 7 | DEFAULT_IGNORE = %w{*~ .svn} 8 | 9 | # Encryption type 10 | attr_accessor :digest 11 | 12 | # Do not use standard exclude/ignore. 13 | attr_accessor :all 14 | 15 | # What file patterns to exclude. 16 | # Exclusion pattens operate on the full pathname. 17 | attr_accessor :exclude 18 | 19 | # What special file patterns to ignore. 20 | # Ignore patterns operate on a path's basename only. 21 | attr_accessor :ignore 22 | 23 | # Layout of digest -- csf or sfv. Default is csf. 24 | attr_accessor :format 25 | 26 | # 27 | def initialize(location, options={}) 28 | raise unless File.directory?(location) 29 | @location = location 30 | file_or_options.each do |h,k| 31 | send("#{h}=", k) 32 | end 33 | end 34 | 35 | # Returns a hash of path => checksum. 36 | # 37 | def chart 38 | @chart ||= ( 39 | h = {} 40 | list.each do |path| 41 | h[path] = checksum(path) 42 | end 43 | h 44 | ) 45 | end 46 | 47 | # List of files in location minus exclusions. 48 | # 49 | def list 50 | @list ||= ( 51 | files = [] 52 | Dir.chdir(@location) do 53 | files += Dir.multiglob_r('**/*') 54 | files -= Dir.multiglob_r(exclusions) 55 | end 56 | files 57 | ) 58 | end 59 | 60 | private 61 | 62 | # Compute exclusions. 63 | # 64 | def exclusions 65 | @_exclusions ||= ( 66 | e = [exclude].flatten 67 | e += DEFAULT_EXCLUDE unless all? 68 | #e += [filename, filename.chomp('~')] if file 69 | e 70 | ) 71 | end 72 | 73 | # Produce hexdigest/cheksum for a file. 74 | # Default digest type is sha1. 75 | 76 | def checksum(file, digest=nil) 77 | return nil unless digest 78 | if FileTest.directory?(file) 79 | @null_string ||= digester(digest).hexdigest("") 80 | else 81 | digester(digest).hexdigest(File.read(file)) 82 | end 83 | end 84 | 85 | # Return a digest class for given +type+. 86 | # Supported digests are: 87 | # 88 | # * md5 89 | # * sha1 90 | # * sha128 (same as sha1) 91 | # * sha256 92 | # * sha512 93 | # 94 | # Default digest type is sha256. 95 | 96 | def digester(type=nil) 97 | require 'openssl' 98 | case type.to_s.downcase 99 | when 'md5' 100 | require 'digest/md5' 101 | ::Digest::MD5 102 | when 'sha128', 'sha1' 103 | require 'digest/sha1' #need? 104 | OpenSSL::Digest::SHA1 105 | when 'sha256' 106 | require 'digest/sha1' #need? 107 | OpenSSL::Digest::SHA256 108 | when 'sha512' 109 | require 'digest/sha1' #need? 110 | OpenSSL::Digest::SHA512 111 | else 112 | raise "unsupported digest #{type}" 113 | end 114 | end 115 | 116 | end 117 | 118 | end 119 | 120 | -------------------------------------------------------------------------------- /work/trash/generator.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | 3 | class Generator 4 | 5 | # File used to store digest/manifest. 6 | attr_accessor :file 7 | 8 | # Encryption type 9 | attr_accessor :digest 10 | 11 | # Do not use standard exclude/ignore. 12 | attr_accessor :all 13 | 14 | # What files to exclude from digest. 15 | attr_accessor :exclude 16 | 17 | # What special files to ignore. 18 | attr_accessor :ignore 19 | 20 | # Directory to use. 21 | attr_accessor :directory 22 | 23 | # Layout of digest -- csf or sfv. Default is csf. 24 | attr_accessor :format 25 | 26 | # Generate manifest. 27 | def generate(out=$stdout) 28 | out << topline_string 29 | output(out) 30 | end 31 | 32 | # 33 | alias_method :all?, :all 34 | 35 | private 36 | 37 | # 38 | 39 | def output(out=$stdout) 40 | Dir.chdir(location) do 41 | rec_output('*', out) 42 | end 43 | end 44 | 45 | # Generate listing on the fly. 46 | 47 | def rec_output(match, out=$stdout) 48 | out.flush 49 | #match = (location == dir ? '*' : File.join(dir,'*')) 50 | files = Dir.glob(match) - exclusions 51 | files.sort! 52 | files.each do |file| 53 | sum = checksum(file,digest) 54 | sum = sum + ' ' if sum 55 | out << "#{sum}#{file}\n" 56 | if File.directory?(file) 57 | rec_output(File.join(file,'*'), out) 58 | end 59 | end 60 | #return out 61 | end 62 | 63 | # Compute exclusions. 64 | 65 | def exclusions 66 | @_exclusions ||= ( 67 | e = [exclude].flatten 68 | e += DEFAULT_EXCLUDE unless all? 69 | e += [filename, filename.chomp('~')] if file 70 | e 71 | ) 72 | end 73 | 74 | # Compute ignores. 75 | 76 | def ignores 77 | end 78 | 79 | # Produce hexdigest/cheksum for a file. 80 | # Default digest type is sha1. 81 | 82 | def checksum(file, digest=nil) 83 | return nil unless digest 84 | if FileTest.directory?(file) 85 | @null_string ||= digester(digest).hexdigest("") # TODO use other means 86 | else 87 | digester(digest).hexdigest(File.read(file)) 88 | end 89 | end 90 | 91 | # Return a digest class for given +type+. 92 | # Supported digests are: 93 | # 94 | # * md5 95 | # * sha1 96 | # * sha128 (same as sha1) 97 | # * sha256 98 | # * sha512 99 | # 100 | # Default digest type is sha256. 101 | 102 | def digester(type=nil) 103 | require 'openssl' 104 | case type.to_s.downcase 105 | when 'md5' 106 | require 'digest/md5' 107 | ::Digest::MD5 108 | when 'sha128', 'sha1' 109 | require 'digest/sha1' #need? 110 | OpenSSL::Digest::SHA1 111 | when 'sha256' 112 | require 'digest/sha1' #need? 113 | OpenSSL::Digest::SHA256 114 | when 'sha512' 115 | require 'digest/sha1' #need? 116 | OpenSSL::Digest::SHA512 117 | else 118 | raise "unsupported digest #{type}" 119 | end 120 | end 121 | 122 | end 123 | 124 | end 125 | 126 | -------------------------------------------------------------------------------- /man/man1/mast.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "MAST" "1" "October 2011" "RubyWorks" "Mast" 5 | . 6 | .SH "NAME" 7 | \fBmast\fR \- manifest generator 8 | . 9 | .SH "SYNOPSIS" 10 | \fBmast [] [\.\.\.]\fR 11 | . 12 | .SH "DESCRIPTION" 13 | The manifest listing tool is used to list, create or update a manifest for a directory (eg\. to define a "package"), or compare a manifest to actual directory contents\. Mast is part of the ProUtils set of tools\. 14 | . 15 | .P 16 | When no command is given, a manifest is dumped to standard out\. If \-\-file is specified, it will generate to that file instead\. 17 | . 18 | .SH "COMMANDS" 19 | . 20 | .TP 21 | \fB\-c\fR, \fB\-\-create\fR 22 | Generate a new manifest\. (default) 23 | . 24 | .TP 25 | \fB\-u\fR, \fB\-\-update\fR 26 | Update an existing manifest\. 27 | . 28 | .TP 29 | \fB\-l\fR, \fB\-\-list\fR 30 | List the files given in the manifest file\. (Use \-f to specify an alternate file\.) 31 | . 32 | .TP 33 | \fB\-D\fR, \fB\-\-diff\fR 34 | Diff manifest file against actual\. 35 | . 36 | .TP 37 | \fB\-n\fR, \fB\-\-new\fR 38 | List existant files that are not given in the manifest\. 39 | . 40 | .TP 41 | \fB\-o\fR, \fB\-\-old\fR 42 | List files given in the manifest but are non\-existent\. 43 | . 44 | .TP 45 | \fB\-v\fR, \fB\-\-verify\fR 46 | Verify that a manifest matches actual\. 47 | . 48 | .TP 49 | \fB\-r\fR, \fB\-\-recent\fR 50 | Verify that a manifest is more recent than actual\. 51 | . 52 | .TP 53 | \fB\-\-clean\fR 54 | Remove non\-manifest files\. (Will ask for confirmation first\.) 55 | . 56 | .TP 57 | \fB\-h\fR, \fB\-\-help\fR 58 | Display this help message\. 59 | . 60 | .SH "OPTIONS" 61 | . 62 | .TP 63 | \fB\-a\fR, \fB\-\-all\fR 64 | Include all files\. This deactivates deafult exclusions so it is possible to make complete list of all contents\. 65 | . 66 | .TP 67 | \fB\-d\fR, \fB\-\-dir\fR 68 | When creating a list include directory paths; by default only files are listed\. 69 | . 70 | .TP 71 | \fB\-b\fR, \fB\-\-bang\fR 72 | Generate manifest using the options from the bang line of the manifest file\. 73 | . 74 | .TP 75 | \fB\-f\fR, \fB\-\-file PATH\fR 76 | Path to manifest file\. This applies to comparison commands\. If not given then the file matching \'MANIFEST\', case\-insensitive and with an optional \'\.txt\' extension, in the current directory is used\. If the path of the manifest file is anything else then the \-\-file option must be specified\. 77 | . 78 | .TP 79 | \fB\-g\fR, \fB\-\-digest TYPE\fR 80 | Include crytographic signiture\. Type can be either md5, sha1, sha128, sha256, or sha512\. 81 | . 82 | .TP 83 | \fB\-x\fR, \fB\-\-exclude PATH\fR 84 | Exclude a file or dir from the manifest matching against full pathname\. You can use \-\-exclude repeatedly\. 85 | . 86 | .TP 87 | \fB\-i\fR, \fB\-\-ignore PATH\fR 88 | Exclude a file or dir from the manifest matching against an entries basename\. You can use \-\-ignore repeatedly\. 89 | . 90 | .TP 91 | \fB\-\-no\-head\fR 92 | Suppress mast header from output\. 93 | . 94 | .TP 95 | \fB\-\-debug\fR 96 | Run command with Ruby\'s $DEBUG flag set to \fBtrue\fR\. 97 | . 98 | .SH "EXAMPLES" 99 | \fBmast\fR 100 | . 101 | .br 102 | \fBmast \-u \-f PUBLISH\fR 103 | . 104 | .SH "SEE ALSO" 105 | ls(1) 106 | -------------------------------------------------------------------------------- /demo/cli/overview.rdoc: -------------------------------------------------------------------------------- 1 | = Overview of Mast Command 2 | 3 | Mast in a manifest and digest generator. 4 | 5 | Lets say we have a directory containing a set of files as follows: 6 | 7 | README.txt 8 | lib/foo.rb 9 | lib/bar.rb 10 | 11 | When we invoke the `mast` command from within that directory, we 12 | will get a complete file listing: 13 | 14 | $ mast 15 | #!mast * 16 | README.txt 17 | lib/bar.rb 18 | lib/foo.rb 19 | 20 | By default directories are not listed. Using the `-d`/`--dir` option will add 21 | them: 22 | 23 | $ mast -d 24 | #!mast -d * 25 | README.txt 26 | lib 27 | lib/bar.rb 28 | lib/foo.rb 29 | 30 | Mast can also be used to generate a file digest with the `-g`/`--digest` option. 31 | For example: 32 | 33 | $ mast -g sha1 34 | #!mast -g sha1 * 35 | fda3484edf8db0684440157ce0b110d784d42704 README.txt 36 | a65255d9e627f654a86f4fd6dfc253566b650b7e lib/bar.rb 37 | 0d6b06bd4b8b334ac3cde825d155795e3ae951cf lib/foo.rb 38 | 39 | Without arguments `mast` will include all files in the current directory 40 | and subdirectories. To limit the manifest to specific files, we can supply 41 | them as arguments on the command line. For example: 42 | 43 | $ mast lib 44 | #!mast lib 45 | lib/bar.rb 46 | lib/foo.rb 47 | 48 | As with any normal shell command we can use file globs: 49 | 50 | $ mast lib/*.rb 51 | #!mast lib/bar.rb lib/foo.rb 52 | lib/bar.rb 53 | lib/foo.rb 54 | 55 | But notice this expands the blob on to the bang line. To keep the glob intact 56 | put it in quotes: 57 | 58 | $ mast 'lib/*.rb' 59 | #!mast lib/*.rb 60 | lib/bar.rb 61 | lib/foo.rb 62 | 63 | To exclude files or directories that might be picked up a file glob, 64 | use the `-x`/`--exclude` option: 65 | 66 | $ mast -x README.txt 67 | #!mast -x README.txt * 68 | lib/bar.rb 69 | lib/foo.rb 70 | 71 | Whereas exclusion elminates exact file and directory matches, `-i`/`--ignore` 72 | can be used to match an file or directory basename: 73 | 74 | $ mast -i foo.rb 75 | #!mast -i foo.rb * 76 | README.txt 77 | lib/bar.rb 78 | 79 | If we would like to save a manifest, we can simply redirect stdout to 80 | a file name: 81 | 82 | $ mast > MANIFEST.txt 83 | 84 | Once saved, `mast` has an update mode via the `-u` option: 85 | 86 | $ mast -u 87 | 88 | By default this works for any manifest file with a name matching "manifest{,.*}", 89 | case insensitvive. An alternate file name can be supplied using the `-f` option. 90 | 91 | Mast can also be used to compare a manifest file to the current directory. A simple 92 | verification that a manifest file matches the current contents of the directory 93 | can be had via the `-v`/`--verify` option: 94 | 95 | $ mast -v 96 | 97 | To see what files are new, that is to say added to the the directory but not 98 | listed in the manifest file, use the `-n/--new` option. For instance, let's 99 | say we have a new file: 100 | 101 | lib/baz.rb 102 | 103 | Then using the `-n` option, `lib/baz.rb` will be listed: 104 | 105 | $ mast -n 106 | lib/baz.rb 107 | 108 | To see what files are _old_, i.e. files listed in the manifest but no longer 109 | in the in the directory, use `-o`/`--old` option. In this case let's remove 110 | a file: 111 | 112 | lib/foo.rb 113 | 114 | And then the `-o` option will list `lib/foo.rb`: 115 | 116 | $ mast -o 117 | lib/foo.rb 118 | 119 | Using `--diff` will pass the content of a manifest file and the current 120 | manifest of the directory to `diff` command. We won't show it here becuase 121 | `diff` produces absolute paths. 122 | 123 | A new manifest can be generated to stdout that uses the options supplied 124 | on the bang line of a manifest file via the `-b`/`--bang` option: 125 | 126 | $ mast -b 127 | #!mast * 128 | README.txt 129 | lib/bar.rb 130 | lib/baz.rb 131 | 132 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Mast 2 | ___ 3 | .-----'---'-. 4 | | | 5 | | | 6 | | | 7 | '-----------' 8 | 9 | 10 | {Homepage}[http://rubyworks.github.com/mast/] . 11 | {Source Code}[http://github.com/rubyworks/mast/] . 12 | {Report Issue}[http://github.com/rubyworks/mast/issues] . 13 | {Mailing List}[http://googlegroups.com/group.rubyworks-mailinglist] . 14 | {IRC Channel}[irc://irc.freenode.net/rubyworks] 15 | 16 | {}[http://travis-ci.org/rubyworks/mast] 17 | 18 | 19 | == DESCRIPTION 20 | 21 | Mast is a commandline utility for generating MANIFEST and DIGEST lists. 22 | It can be useful in conjunction with packaging tools, or as a stand-alone 23 | tool for monitoring file changes. 24 | 25 | 26 | == FEATURES 27 | 28 | * Intuitive command-line interface --simply specify what to include. 29 | * Self-referential header makes updating manifests easy. 30 | * Checksum digests can highlight file changes. 31 | 32 | 33 | == USAGE 34 | 35 | Mast makes the process of generating manifests very easy, and even allows 36 | manifests to be updated without repeating inclusion/exclusion criteria by 37 | storing the command parameters in a comment line at the top of the generated 38 | output. 39 | 40 | Lets try a simple example. Lets say we have the following folder structure, 41 | using ls -R we see: 42 | 43 | $ ls -R 44 | .: 45 | demo_rtar mint 46 | 47 | ./demo_rtar: 48 | Lorem_ipsum.txt lib meta web 49 | 50 | ./demo_rtar/lib: 51 | demo_rock 52 | 53 | ./demo_rtar/lib/demo_rock: 54 | tryme.rb 55 | 56 | ./demo_rtar/meta: 57 | data 58 | 59 | ./demo_rtar/web: 60 | index.html rocklobster.jpg 61 | 62 | ./mint: 63 | loremipsum.txt tryme.rb 64 | 65 | Now lets look at the same folder via 'mast'. 66 | 67 | $ mast 68 | #!mast 69 | demo_rtar 70 | demo_rtar/Lorem_ipsum.txt 71 | demo_rtar/lib 72 | demo_rtar/lib/demo_rock 73 | demo_rtar/lib/demo_rock/tryme.rb 74 | demo_rtar/meta 75 | demo_rtar/meta/data 76 | demo_rtar/web 77 | demo_rtar/web/index.html 78 | demo_rtar/web/rocklobster.jpg 79 | mint 80 | mint/loremipsum.txt 81 | mint/tryme.rb 82 | 83 | As you can see it has listed all the files contained in the current folder. 84 | Notice also the first line is empty except for the '#' character. This is 85 | a standard shell comment mark. We can specify special criteria to the mast 86 | command and these options will be reflected on this line. For example, lets 87 | say the mint directory is extraneous and we do not want it included in the 88 | list of files. 89 | 90 | $ mast -x mint 91 | #!mast -x mint 92 | demo_rtar 93 | demo_rtar/Lorem_ipsum.txt 94 | demo_rtar/lib 95 | demo_rtar/lib/demo_rock 96 | demo_rtar/lib/demo_rock/tryme.rb 97 | demo_rtar/meta 98 | demo_rtar/meta/data 99 | demo_rtar/web 100 | demo_rtar/web/index.html 101 | demo_rtar/web/rocklobster.jpg 102 | 103 | So you can see how the commandline options carry over to the top comment line 104 | of the ouput. The advantage of this is that if you save the output to a standard 105 | location, i.e. a file named MANIFEST or meta/manifest with an optional `.txt` 106 | prefix (case insensitive), then you can automaitcally update the file by calling 107 | mast --update. 108 | 109 | $ mast -x mint > MANIFEST 110 | 111 | $ mast --update 112 | MANIFEST updated. 113 | 114 | You can also add a checksum to the file list to create a *DIGEST*. 115 | 116 | $ mast -x mint -g sha1 117 | 118 | Mast also provides options for ignoring files based on their basename, as well 119 | as omitting default excludes and ignores so that all files are lists. Use the 120 | --help option for more details. 121 | 122 | 123 | == HOW TO INSTALL 124 | 125 | To install with RubyGems simply open a console and type: 126 | 127 | $ gem install mast 128 | 129 | For a traditional site installation use Ruby Setup (gem install setup). 130 | 131 | $ tar -xvzf mast-1.3.0.tar.gz 132 | $ cd mast-1.3.0.tar.gz 133 | $ sudo setup.rb 134 | 135 | See {Ruby Setup}[http://rubyworks.github.com/setup] for more information. 136 | 137 | 138 | == COPYRIGHT 139 | 140 | Copyright (c) 2009 Rubyworks 141 | 142 | Mast is distributable according to the terms of the BSD-2-Clause license. 143 | 144 | See COPYING.rdoc for details. 145 | -------------------------------------------------------------------------------- /DEMO.rdoc: -------------------------------------------------------------------------------- 1 | = Overview of Mast Command 2 | 3 | Mast in a manifest and digest generator. 4 | 5 | Lets say we have a directory containing a set of files as follows: 6 | 7 | README.txt 8 | lib/foo.rb 9 | lib/bar.rb 10 | 11 | When we invoke the `mast` command from within that directory, we 12 | will get a complete file listing: 13 | 14 | $ mast 15 | #!mast * 16 | README.txt 17 | lib/bar.rb 18 | lib/foo.rb 19 | 20 | By default directories are not listed. Using the `-d`/`--dir` option will add 21 | them: 22 | 23 | $ mast -d 24 | #!mast -d * 25 | README.txt 26 | lib 27 | lib/bar.rb 28 | lib/foo.rb 29 | 30 | Mast can also be used to generate a file digest with the `-g`/`--digest` option. 31 | For example: 32 | 33 | $ mast -g sha1 34 | #!mast -g sha1 * 35 | fda3484edf8db0684440157ce0b110d784d42704 README.txt 36 | a65255d9e627f654a86f4fd6dfc253566b650b7e lib/bar.rb 37 | 0d6b06bd4b8b334ac3cde825d155795e3ae951cf lib/foo.rb 38 | 39 | Without arguments `mast` will include all files in the current directory 40 | and subdirectories. To limit the manifest to specific files, we can supply 41 | them as arguments on the command line. For example: 42 | 43 | $ mast lib 44 | #!mast lib 45 | lib/bar.rb 46 | lib/foo.rb 47 | 48 | As with any normal shell command we can use file globs: 49 | 50 | $ mast lib/*.rb 51 | #!mast lib/bar.rb lib/foo.rb 52 | lib/bar.rb 53 | lib/foo.rb 54 | 55 | But notice this expands the blob on to the bang line. To keep the glob intact 56 | put it in quotes: 57 | 58 | $ mast 'lib/*.rb' 59 | #!mast lib/*.rb 60 | lib/bar.rb 61 | lib/foo.rb 62 | 63 | To exclude files or directories that might be picked up a file glob, 64 | use the `-x`/`--exclude` option: 65 | 66 | $ mast -x README.txt 67 | #!mast -x README.txt * 68 | lib/bar.rb 69 | lib/foo.rb 70 | 71 | Whereas exclusion elminates exact file and directory matches, `-i`/`--ignore` 72 | can be used to match an file or directory basename: 73 | 74 | $ mast -i foo.rb 75 | #!mast -i foo.rb * 76 | README.txt 77 | lib/bar.rb 78 | 79 | If we would like to save a manifest, we can simply redirect stdout to 80 | a file name: 81 | 82 | $ mast > MANIFEST.txt 83 | 84 | Once saved, `mast` has an update mode via the `-u` option: 85 | 86 | $ mast -u 87 | 88 | By default this works for any manifest file with a name matching "manifest{,.*}", 89 | case insensitvive. An alternate file name can be supplied using the `-f` option. 90 | 91 | Mast can also be used to compare a manifest file to the current directory. A simple 92 | verification that a manifest file matches the current contents of the directory 93 | can be had via the `-v`/`--verify` option: 94 | 95 | $ mast -v 96 | 97 | To see what files are new, that is to say added to the the directory but not 98 | listed in the manifest file, use the `-n/--new` option. For instance, let's 99 | say we have a new file: 100 | 101 | lib/baz.rb 102 | 103 | Then using the `-n` option, `lib/baz.rb` will be listed: 104 | 105 | $ mast -n 106 | lib/baz.rb 107 | 108 | To see what files are _old_, i.e. files listed in the manifest but no longer 109 | in the in the directory, use `-o`/`--old` option. In this case let's remove 110 | a file: 111 | 112 | lib/foo.rb 113 | 114 | And then the `-o` option will list `lib/foo.rb`: 115 | 116 | $ mast -o 117 | lib/foo.rb 118 | 119 | Using `--diff` will pass the content of a manifest file and the current 120 | manifest of the directory to `diff` command. We won't show it here becuase 121 | `diff` produces absolute paths. 122 | 123 | A new manifest can be generated to stdout that uses the options supplied 124 | on the bang line of a manifest file via the `-b`/`--bang` option: 125 | 126 | $ mast -b 127 | #!mast * 128 | README.txt 129 | lib/bar.rb 130 | lib/baz.rb 131 | 132 | 133 | == Manifest Generation 134 | 135 | Lets say we have a directory containing a set of files as follows: 136 | 137 | README.txt 138 | lib/foo.rb 139 | lib/bar.rb 140 | 141 | We can crate a new Manifest object, utilizing a StringIO object to catch 142 | the output. 143 | 144 | out = '' 145 | 146 | manifest = Mast::Manifest.new(:io=>StringIO.new(out)) 147 | 148 | If we call the generate method, then the output should list the 149 | above file along with a generic shebang header. 150 | 151 | manifest.generate 152 | 153 | list = out.split("\n").sort 154 | 155 | list.assert == ['#!mast *','README.txt','lib/bar.rb','lib/foo.rb'] 156 | 157 | 158 | -------------------------------------------------------------------------------- /lib/mast/core_ext.rb: -------------------------------------------------------------------------------- 1 | # Metaclass extensions for core File class. 2 | # 3 | class File #:nodoc: 4 | 5 | # Is a file a gzip file? 6 | # 7 | def self.gzip?(file) 8 | open(file,'rb') { |f| 9 | return false unless f.getc == 0x1f 10 | return false unless f.getc == 0x8b 11 | } 12 | true 13 | end 14 | 15 | # Reads in a file, removes blank lines and remarks 16 | # (lines starting with '#') and then returns 17 | # an array of all the remaining lines. 18 | # 19 | # CREDIT: Trans 20 | def self.read_list(filepath, chomp_string='') 21 | farr = nil 22 | farr = read(filepath).split("\n") 23 | farr.collect! { |line| 24 | l = line.strip.chomp(chomp_string) 25 | (l.empty? or l[0,1] == '#') ? nil : l 26 | } 27 | farr.compact 28 | end 29 | 30 | # Return the path shared. 31 | def self.sharedpath(file1, file2) 32 | afile1 = file1.split(/\/\\/) 33 | afile2 = file2.split(/\/\\/) 34 | overlap = [] 35 | i = 0; e1, e2 = afile1[i], afile2[i] 36 | while e1 && e2 && e1 == e2 37 | overlap << e1 38 | i += 1; e1, e2 = afile1[i], afile2[i] 39 | end 40 | return overlap.empty? ? false : overlap 41 | end 42 | 43 | # Is path1 a parent directory of path2. 44 | def self.parent?(file1, file2) 45 | return false if File.identical?(file1, file2) 46 | afile1 = file1.split(/(\/|\\)/) 47 | afile2 = file2.split(/(\/|\\)/) 48 | overlap = [] 49 | i = 0; e1, e2 = afile1[i], afile2[i] 50 | while e1 && e2 && e1 == e2 51 | overlap << e1 52 | i += 1; e1, e2 = afile1[i], afile2[i] 53 | end 54 | return (overlap == afile1) 55 | end 56 | 57 | # Reduce a list of files so there is no overlapping 58 | # directory entries. This is useful when recursively 59 | # descending a directory listing, so as to avoid and 60 | # repeat entries. 61 | # 62 | # TODO: Maybe globbing should occur in here too? 63 | # 64 | def self.reduce(*list) 65 | # TODO: list = list.map{ |f| File.cleanpath(f) } 66 | newlist = list.dup 67 | list.each do |file1| 68 | list.each do |file2| 69 | if parent?(file1, file2) 70 | newlist.delete(file2) 71 | end 72 | end 73 | end 74 | newlist 75 | end 76 | 77 | end 78 | 79 | # Metaclass extensions for core Dir class. 80 | # 81 | class Dir #:nodoc: 82 | 83 | # Like +glob+ but can take multiple patterns. 84 | # 85 | # Dir.multiglob( '*.rb', '*.py' ) 86 | # 87 | # Rather then constants for options multiglob accepts a trailing options 88 | # hash of symbol keys. 89 | # 90 | # :noescape File::FNM_NOESCAPE 91 | # :casefold File::FNM_CASEFOLD 92 | # :pathname File::FNM_PATHNAME 93 | # :dotmatch File::FNM_DOTMATCH 94 | # :strict File::FNM_PATHNAME && File::FNM_DOTMATCH 95 | # 96 | # It also has an option for recurse. 97 | # 98 | # :recurse Recurively include contents of directories. 99 | # 100 | # For example 101 | # 102 | # Dir.multiglob( '*', :recurse => true ) 103 | # 104 | # would have the same result as 105 | # 106 | # Dir.multiglob('**/*') 107 | # 108 | def self.multiglob(*patterns) 109 | options = (Hash === patterns.last ? patterns.pop : {}) 110 | 111 | if options.delete(:recurse) 112 | #patterns += patterns.collect{ |f| File.join(f, '**', '**') } 113 | multiglob_r(*patterns) 114 | end 115 | 116 | bitflags = 0 117 | bitflags |= File::FNM_NOESCAPE if options[:noescape] 118 | bitflags |= File::FNM_CASEFOLD if options[:casefold] 119 | bitflags |= File::FNM_PATHNAME if options[:pathname] or options[:strict] 120 | bitflags |= File::FNM_DOTMATCH if options[:dotmatch] or options[:strict] 121 | 122 | patterns = [patterns].flatten.compact 123 | 124 | if options[:recurse] 125 | patterns += patterns.collect{ |f| File.join(f, '**', '**') } 126 | end 127 | 128 | files = [] 129 | files += patterns.collect{ |pattern| Dir.glob(pattern, bitflags) }.flatten.uniq 130 | 131 | return files 132 | end 133 | 134 | # The same as +multiglob+, but recusively includes directories. 135 | # 136 | # Dir.multiglob_r( 'folder' ) 137 | # 138 | # is equivalent to 139 | # 140 | # Dir.multiglob( 'folder', :recurse=>true ) 141 | # 142 | # The effect of which is 143 | # 144 | # Dir.multiglob( 'folder', 'folder/**/**' ) 145 | # 146 | def self.multiglob_r(*patterns) 147 | options = (Hash === patterns.last ? patterns.pop : {}) 148 | matches = multiglob(*patterns) 149 | directories = matches.select{ |m| File.directory?(m) } 150 | matches += directories.collect{ |d| multiglob_r(File.join(d, '**'), options) }.flatten 151 | matches.uniq 152 | #options = (Hash === patterns.last ? patterns.pop : {}) 153 | #options[:recurse] = true 154 | #patterns << options 155 | #multiglob(*patterns) 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | module DotRuby 6 | 7 | # 8 | class GemSpec 9 | 10 | # For which revision of .ruby is this gemspec intended? 11 | REVISION = 0 12 | 13 | # 14 | PATTERNS = { 15 | :bin_files => 'bin/*', 16 | :lib_files => 'lib/{**/}*.rb', 17 | :ext_files => 'ext/{**/}extconf.rb', 18 | :doc_files => '*.{txt,rdoc,md,markdown,tt,textile}', 19 | :test_files => '{test/{**/}*_test.rb,spec/{**/}*_spec.rb}' 20 | } 21 | 22 | # 23 | def self.instance 24 | new.to_gemspec 25 | end 26 | 27 | attr :metadata 28 | 29 | attr :manifest 30 | 31 | # 32 | def initialize 33 | @metadata = YAML.load_file('.ruby') 34 | @manifest = Dir.glob('manifest{,.txt}', File::FNM_CASEFOLD).first 35 | 36 | if @metadata['revision'].to_i != REVISION 37 | warn "You have the wrong revision. Trying anyway..." 38 | end 39 | end 40 | 41 | # 42 | def scm 43 | @scm ||= \ 44 | case 45 | when File.directory?('.git') 46 | :git 47 | end 48 | end 49 | 50 | # 51 | def files 52 | @files ||= \ 53 | #glob_files[patterns[:files]] 54 | case 55 | when manifest 56 | File.readlines(manifest). 57 | map{ |line| line.strip }. 58 | reject{ |line| line.empty? || line[0,1] == '#' } 59 | when scm == :git 60 | `git ls-files -z`.split("\0") 61 | else 62 | Dir.glob('{**/}{.*,*}') # TODO: be more specific using standard locations ? 63 | end.select{ |path| File.file?(path) } 64 | end 65 | 66 | # 67 | def glob_files(pattern) 68 | Dir.glob(pattern).select { |path| 69 | File.file?(path) && files.include?(path) 70 | } 71 | end 72 | 73 | # 74 | def patterns 75 | PATTERNS 76 | end 77 | 78 | # 79 | def executables 80 | @executables ||= \ 81 | glob_files(patterns[:bin_files]).map do |path| 82 | File.basename(path) 83 | end 84 | end 85 | 86 | def extensions 87 | @extensions ||= \ 88 | glob_files(patterns[:ext_files]).map do |path| 89 | File.basename(path) 90 | end 91 | end 92 | 93 | # 94 | def name 95 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 96 | end 97 | 98 | # 99 | def to_gemspec 100 | Gem::Specification.new do |gemspec| 101 | gemspec.name = name 102 | gemspec.version = metadata['version'] 103 | gemspec.summary = metadata['summary'] 104 | gemspec.description = metadata['description'] 105 | 106 | metadata['authors'].each do |author| 107 | gemspec.authors << author['name'] 108 | 109 | if author.has_key?('email') 110 | if gemspec.email 111 | gemspec.email << author['email'] 112 | else 113 | gemspec.email = [author['email']] 114 | end 115 | end 116 | end 117 | 118 | gemspec.licenses = metadata['copyrights'].map{ |c| c['license'] }.compact 119 | 120 | metadata['requirements'].each do |req| 121 | name = req['name'] 122 | version = req['version'] 123 | groups = req['groups'] || [] 124 | 125 | case version 126 | when /^(.*?)\+$/ 127 | version = ">= #{$1}" 128 | when /^(.*?)\-$/ 129 | version = "< #{$1}" 130 | when /^(.*?)\~$/ 131 | version = "~> #{$1}" 132 | end 133 | 134 | if groups.empty? or groups.include?('runtime') 135 | # populate runtime dependencies 136 | if gemspec.respond_to?(:add_runtime_dependency) 137 | gemspec.add_runtime_dependency(name,*version) 138 | else 139 | gemspec.add_dependency(name,*version) 140 | end 141 | else 142 | # populate development dependencies 143 | if gemspec.respond_to?(:add_development_dependency) 144 | gemspec.add_development_dependency(name,*version) 145 | else 146 | gemspec.add_dependency(name,*version) 147 | end 148 | end 149 | end 150 | 151 | # convert external dependencies into a requirements 152 | if metadata['external_dependencies'] 153 | ##gemspec.requirements = [] unless metadata['external_dependencies'].empty? 154 | metadata['external_dependencies'].each do |req| 155 | gemspec.requirements << req.to_s 156 | end 157 | end 158 | 159 | # determine homepage from resources 160 | homepage = metadata['resources'].find{ |key, url| key =~ /^home/ } 161 | gemspec.homepage = homepage.last if homepage 162 | 163 | gemspec.require_paths = metadata['load_path'] || ['lib'] 164 | gemspec.post_install_message = metadata['install_message'] 165 | 166 | # RubyGems specific metadata 167 | gemspec.files = files 168 | gemspec.extensions = extensions 169 | gemspec.executables = executables 170 | 171 | if Gem::VERSION < '1.7.' 172 | gemspec.default_executable = gemspec.executables.first 173 | end 174 | 175 | gemspec.test_files = glob_files(patterns[:test_files]) 176 | 177 | unless gemspec.files.include?('.document') 178 | gemspec.extra_rdoc_files = glob_files(patterns[:doc_files]) 179 | end 180 | end 181 | end 182 | 183 | end #class GemSpec 184 | 185 | end 186 | 187 | DotRuby::GemSpec.instance 188 | -------------------------------------------------------------------------------- /lib/mast/cli.rb: -------------------------------------------------------------------------------- 1 | require 'mast' 2 | require 'optparse' 3 | 4 | module Mast 5 | 6 | #ARGVO = ARGV.dup 7 | 8 | # Manifest Console Command 9 | # 10 | class Cli 11 | 12 | def self.run 13 | new.run 14 | end 15 | 16 | DIGESTS = [:md5, :sha1, :sha128, :sha256, :sha512] 17 | 18 | attr_accessor :quiet 19 | 20 | # Options for Manifest class taken from commandline arguments. 21 | attr :options 22 | 23 | # 24 | def initialize 25 | @options = {} 26 | @options[:all] = false 27 | @options[:file] = nil 28 | @options[:bang] = nil 29 | @options[:digest] = nil 30 | @options[:exclude] = [] 31 | @options[:ignore] = [] 32 | @options[:include] = [] 33 | 34 | @command = [] 35 | 36 | @quiet = false 37 | end 38 | 39 | # 40 | def run(argv=nil) 41 | begin 42 | run_command(argv) 43 | rescue => err 44 | raise err if $DEBUG 45 | report err 46 | end 47 | end 48 | 49 | # Run command. 50 | def run_command(argv) 51 | argv = (argv || ARGV).dup 52 | 53 | @original_arguments = argv.dup 54 | 55 | option_parser.parse!(argv) 56 | 57 | @options[:include] = argv.empty? ? nil : argv #.dup 58 | 59 | if @command.size > 1 60 | raise ArgumentError, "Please issue only one command." 61 | end 62 | 63 | case @command.first 64 | when :help then help 65 | when :create then generate 66 | when :update then update 67 | when :list then list 68 | when :diff then diff 69 | when :new then new 70 | when :old then old 71 | when :verify then verify 72 | when :clean then clean 73 | when :recent then recent 74 | else 75 | generate 76 | end 77 | end 78 | 79 | # Parse command line options. 80 | def option_parser 81 | OptionParser.new do |opt| 82 | opt.on "-f", "--file FILE", "Path to manifest file. Looks for file matching /MANIFEST(|.txt)/i by default." do |file| 83 | @options[:file] = file 84 | end 85 | opt.on "-g", "--digest TYPE", "Include cryptographic signature. Type can be either md5, sha1, sha128, sha256, or sha512." do |digest| 86 | @options[:digest] = digest 87 | end 88 | opt.on "-x", "--exclude GLOB", "Exclude file or dir from the manifest matching against full pathname. Can be used repeatedly." do |glob| 89 | @options[:exclude] << glob 90 | end 91 | opt.on "-i", "--ignore GLOB", 92 | "Exclude file or dir from manifest matching against an entry's basename. Can be used repeatedly." do |glob| 93 | @options[:ignore] << glob 94 | end 95 | opt.on "-a", "--all", "Include all files. This deactivates default exclusions so it is possible to make complete list of all contents." do |bool| 96 | @options[:all] = true 97 | end 98 | opt.on "--bang", "-b", "Generate manifest using the options from the bang line of the manifest file." do |bool| 99 | @options[:bang] = true 100 | end 101 | opt.on "--dir", "-d", "When creating a list include directory paths; by default only files are listed." do |bool| 102 | @options[:dir] = bool 103 | end 104 | opt.on "--[no-]head", "Suppress mast header from output." do |bool| 105 | @options[:headless] = !bool 106 | end 107 | opt.on "-c", "--create", "Generate a new manifest. (default)" do 108 | @command << :create 109 | end 110 | opt.on "-u", "--update", "Update an existing manifest." do 111 | @command << :update 112 | end 113 | opt.on "-l", "--list", "List the files given in the manifest file. (Use -f to specify an alternate file.)" do 114 | @command << :list 115 | end 116 | opt.on "-D", "--diff", "Diff manifest file against actual." do 117 | @command << :diff 118 | end 119 | opt.on "-n", "--new", "List existent files that are not given in the manifest." do 120 | @command << :new 121 | end 122 | opt.on "-o", "--old", "List files given in the manifest but are non-existent." do 123 | @command << :old 124 | end 125 | opt.on "-v", "--verify", "Verify that a manifest matches actual." do 126 | @command << :verify 127 | end 128 | opt.on "--clean", "Remove non-manifest files. (Will ask for confirmation first.)" do 129 | @command << :clean 130 | end 131 | opt.on "-r", "--recent", "Verify that a manifest is more recent than actual." do 132 | @command << :recent 133 | end 134 | opt.on "-h", "--help", "Display help manual." do 135 | @command << :help 136 | end 137 | opt.on "-H", "Display help summary." do 138 | puts opt; exit 139 | end 140 | opt.on "-q", "--quiet", "Suppress all extraneous output." do 141 | @quiet = true 142 | end 143 | opt.on "--debug", "Run in debug mode." do 144 | $DEBUG = true 145 | end 146 | end 147 | end 148 | 149 | # Default command -- output manifest. 150 | def generate 151 | #if file 152 | # update 153 | #else 154 | manifest.generate 155 | #end 156 | end 157 | 158 | # Update a MANIFEST file for this package. 159 | def update 160 | if manifest.verify 161 | manifest.touch 162 | else 163 | begin 164 | diff = manifest.diff 165 | file = manifest.update 166 | rescue Manifest::NoManifestError => e 167 | puts e.message 168 | exit -1 169 | end 170 | report_difference(diff) 171 | #report_updated(file) 172 | end 173 | end 174 | 175 | alias_method :up, :update 176 | 177 | # List files in manifest file. 178 | def list 179 | puts manifest.filelist 180 | end 181 | 182 | # 183 | #def show 184 | # puts manifest.showlist 185 | #end 186 | 187 | # Show diff comparison between listed and actual. 188 | def diff 189 | result = manifest.diff 190 | report_difference(result) 191 | end 192 | 193 | # Files listed in manifest, but not found. 194 | def old 195 | list = manifest.whatsold 196 | unless list.empty? 197 | report_whatsold(list) 198 | end 199 | end 200 | 201 | # Files found, but not listed in manifest. 202 | def new 203 | list = manifest.whatsnew 204 | unless list.empty? 205 | report_whatsnew(list) 206 | end 207 | end 208 | 209 | # 210 | def verify 211 | check = manifest.verify 212 | report_verify(check) 213 | exit -1 unless check 214 | end 215 | 216 | # Clean (or clobber if you prefer) non-manifest files. 217 | def clean 218 | answer = confirm_clean(manifest.cleanlist) 219 | case answer.downcase 220 | when 'y', 'yes' 221 | manifest.clean 222 | else 223 | report_cancelled('Clean') 224 | exit! 225 | end 226 | end 227 | 228 | # Verify manifest, then check to see that it is not older than files 229 | # it lists. 230 | def recent 231 | check = manifest.verify 232 | if !check 233 | report_verify(check) 234 | exit -1 235 | end 236 | if !FileUtils.uptodate?(manifest.file, manifest.filelist) 237 | report_outofdate 238 | exit -1 239 | end 240 | end 241 | 242 | # Display command help information. 243 | def help 244 | report_help 245 | end 246 | 247 | private 248 | 249 | # 250 | def manifest 251 | @manifest ||= Manifest.new(options) 252 | #@manifest ||= ( 253 | # begin 254 | # manifest = Manifest.open(file, options) 255 | # manifest 256 | # rescue LoadError 257 | # report_manifest_missing 258 | # exit 0 259 | # end 260 | #) 261 | end 262 | 263 | # Quiet opertation? 264 | def quiet? 265 | @quiet 266 | end 267 | 268 | # Get confirmation for clean. 269 | def confirm_clean(list) 270 | puts list.join("\n") 271 | ask("The above files will be removed. Continue? [yN]") 272 | end 273 | 274 | # Get confirmation for clobber. 275 | def confirm_clobber(list) 276 | puts list.join("\n") 277 | ask("The above files will be removed. Continue? [yN]") 278 | end 279 | 280 | # 281 | def report(message) 282 | $stderr << "#{message}\n" unless quiet? 283 | end 284 | 285 | # Report manifest created. 286 | def report_created(file) 287 | file = File.basename(file) 288 | report "#{file} created." 289 | end 290 | 291 | # Report manifest updated. 292 | def report_updated(file) 293 | if file 294 | file = File.basename(file) 295 | report "#{file} updated." 296 | else 297 | report "Manifest file doesn't exit." 298 | end 299 | end 300 | 301 | # Display diff between file and actual. 302 | #-- 303 | # TODO What about checkmode? 304 | #++ 305 | def report_difference(result) 306 | output = nil 307 | if pass = result.empty? 308 | if @checkmode 309 | output = justified('Manifest', '[PASS]') + "\n" 310 | else 311 | #"Manifest is current." 312 | end 313 | else 314 | output = result 315 | end 316 | puts output if output 317 | end 318 | 319 | # Report missing manifest file. 320 | def report_whatsnew(list) 321 | puts list.join("\n") 322 | end 323 | 324 | # 325 | def report_whatsold(list) 326 | puts list.join("\n") 327 | end 328 | 329 | # Show help. 330 | def report_help 331 | doc = false 332 | man_page = File.dirname(__FILE__) + '/../../man/man1/mast.1' 333 | ronn_file = File.dirname(__FILE__) + '/../../man/man1/mast.1.ronn' 334 | if File.exist?(man_page) 335 | system "man #{man_page}" || puts(File.read(ronn_file)) 336 | else 337 | puts option_parser 338 | end 339 | end 340 | 341 | # Report missing manifest file. 342 | def report_manifest_missing 343 | report "No manifest file." 344 | end 345 | 346 | # Report action cancelled. 347 | def report_cancelled(action) 348 | report "#{action} cancelled." 349 | end 350 | 351 | # Report manifest overwrite. 352 | def report_overwrite(manifest) 353 | report "#{manifest.filename} already exists." 354 | end 355 | 356 | # Warn that a manifest already exist higher in this hierarchy. 357 | def report_warn_shadowing(manifest) 358 | report "Shadowing #{manifest.file}." 359 | end 360 | 361 | # 362 | def report_verify(check) 363 | if check 364 | report "Manifest is good." 365 | else 366 | report "Manifest is bad!" 367 | end 368 | end 369 | 370 | # 371 | def report_outofdate 372 | report "Manifest is older than listed file(s)." 373 | end 374 | 375 | # 376 | def ask(prompt=nil) 377 | $stdout << "#{prompt}" 378 | $stdout.flush 379 | $stdin.gets.chomp! 380 | end 381 | 382 | end 383 | 384 | end 385 | -------------------------------------------------------------------------------- /work/trash/manifest2.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | require 'fileutils' 3 | require 'getoptlong' 4 | require 'shellwords' 5 | 6 | # Manifest stores a list of package files, and optionally checksums. 7 | # 8 | # The class can be used to create and compare package manifests and digests. 9 | # 10 | # TODO: 11 | # - Integrate file signing and general manifest better (?) 12 | # - Digester is in sign.rb too. Dry-up? 13 | # - Is it problematic to add a digest to the manifest? 14 | # - This needs some TLC. Eg. diff is shelling out, but it would 15 | # be better if internalized. 16 | # - Could just use some over all belt tightening. 17 | # 18 | class Manifest 19 | 20 | # Manifest file overwrite error. 21 | # 22 | class OverwriteError < Exception 23 | end 24 | 25 | # No Manifest File Error. 26 | # 27 | class NoManifestError < LoadError 28 | end 29 | 30 | DEFAULT_FILE = 'manifest{,.txt}' 31 | DEFAULT_EXCLUDE = %w{ InstalledFiles .config *~ CVS _darcs .svn .git } 32 | 33 | # Possible file name (was for Fileable?). 34 | def self.filename 35 | DEFAULT_FILE 36 | end 37 | 38 | def self.open(file=nil, options={}) 39 | unless file 40 | file = Dir.glob(filename, File::FNM_CASEFOLD).first 41 | raise LoadError, "Manifest file is required." unless file 42 | end 43 | options.update(:file => file) 44 | new(options) 45 | end 46 | 47 | # File used to store digest/manifest. 48 | attr_accessor :file 49 | 50 | # Encryption type 51 | attr_accessor :digest 52 | 53 | # Do not exclude standard exclusions. 54 | attr_accessor :all 55 | alias_method :all?, :all 56 | 57 | # What files to exclude from digest. 58 | attr_accessor :exclude 59 | alias_method :ignore, :exclude 60 | 61 | # Directory to use. 62 | attr_accessor :directory 63 | 64 | # Layout of digest, csf or sfv. Default is csf. 65 | attr_accessor :format 66 | 67 | # Files and checksums listed in file. 68 | attr_reader :list 69 | 70 | # New Digest object. 71 | def initialize(options={}) 72 | #if File.directory?(location) 73 | # if file = Dir[File.join(location, DEFAULT_FILE)].first 74 | # @file = file 75 | # else 76 | # end 77 | #else 78 | # read(@file = location) 79 | #end 80 | 81 | @exclude = [] 82 | 83 | change_options(options) 84 | 85 | #read(file) if file 86 | #else 87 | #if file = Dir.glob(self.class.filename)[0] 88 | # @file = file 89 | #else 90 | # @file = DEFAULT_FILE 91 | #end 92 | end 93 | end 94 | 95 | # Set options. 96 | def change_options(opts) 97 | opts.each do |k,v| 98 | k = k.to_s.downcase 99 | send("#{k}=",v||send(k)) 100 | end 101 | #@file = options[:file] || @file 102 | #@digest = options[:digest] || @digest 103 | #@all = options[:all] || @all 104 | #@exclude = options[:exclude] || options[:ignore] || @exclude 105 | #@exclude = [@exclude].flatten.compact 106 | end 107 | 108 | # 109 | def file? 110 | @read_from_file 111 | end 112 | 113 | # Create a digest/manifest. 114 | def create(options=nil) 115 | change_options(options) if options 116 | generate 117 | end 118 | 119 | # Update a MANIFEST/DIGEST file. 120 | def update(options=nil) 121 | change_options(options) if options 122 | raise NoManifestError unless file and FileTest.file?(file) 123 | save 124 | end 125 | 126 | # Generate MANIFEST and save as file. 127 | def save #(options=nil) 128 | #change_options(options) if options 129 | File.open(file, 'w') do |f| 130 | f << topline_string 131 | output(f) 132 | end 133 | return file 134 | end 135 | 136 | # Diff file against actual files. 137 | # TODO: do this internally. 138 | def diff 139 | raise NoManifestError unless file and FileTest.file?(file) 140 | manifest = save_temporary_manifest 141 | begin 142 | result = `diff -du #{file} #{manifest.file}` 143 | ensure 144 | FileUtils.rm(manifest.file) 145 | end 146 | # pass = result.empty? 147 | return result 148 | end 149 | 150 | # Files listed in manifest, but not found. 151 | def whatsold 152 | #raise ManifestMissing unless file 153 | filelist - files 154 | end 155 | 156 | # Files found, but not listed in manifest. 157 | def whatsnew 158 | #raise ManifestMissing unless file 159 | files - (filelist + [filename]) 160 | end 161 | 162 | # Clean non-manifest files. 163 | def clean 164 | cfiles, cdirs = clean_files.partition{ |f| !File.directory?(f) } 165 | FileUtils.rm(cfiles) 166 | FileUtils.rmdir(cdirs) 167 | end 168 | 169 | # 170 | def clean_files 171 | keep = Dir.glob('*').select{|f| File.directory?(f)} 172 | keep << filename # keep manifest 173 | Dir.glob('**/*') - (files + keep) 174 | end 175 | 176 | # # Clobber non-manifest files. 177 | # # 178 | # def clobber 179 | # clobber_files.each{ |f| rm_r(f) if File.exist?(f) } 180 | # end 181 | # 182 | # #-- 183 | # # TODO Should clobber work off the manifest file itself? 184 | # #++ 185 | # def clobber_files 186 | # keep = filelist # + [info.manifest] 187 | # Dir.glob('**/*') - keep 188 | # end 189 | 190 | # Manifest's filename. 191 | def filename 192 | File.basename(file) 193 | end 194 | 195 | # File's location. 196 | def location 197 | #if directory 198 | # directory 199 | #if file 200 | # File.dirname(file) 201 | #else 202 | @location = @directory || Dir.pwd 203 | #end 204 | end 205 | 206 | # List of files as given in the file. 207 | def filelist 208 | list.keys.sort 209 | end 210 | 211 | # Generate manifest. 212 | def generate(out=$stdout) 213 | out << topline_string 214 | output(out) 215 | end 216 | 217 | # 218 | def output(out=$stdout) 219 | Dir.chdir(location) do 220 | rec_output('*', out) 221 | end 222 | end 223 | 224 | # Generate listing on the fly. 225 | def rec_output(match, out=$stdout) 226 | out.flush 227 | #match = (location == dir ? '*' : File.join(dir,'*')) 228 | files = Dir.glob(match) - exclusions 229 | files.sort! 230 | files.each do |file| 231 | sum = checksum(file,digest) 232 | sum = sum + ' ' if sum 233 | out << "#{sum}#{file}\n" 234 | if File.directory?(file) 235 | rec_output(File.join(file,'*'), out) 236 | end 237 | end 238 | #return out 239 | end 240 | private :rec_output 241 | 242 | 243 | # List files in package. 244 | 245 | def files #(update=false) 246 | #remove = (update ? (exclude + topline.exclude) : exclude) 247 | #remove = exclude 248 | #remove += DEFAULT_EXCLUDE unless all? 249 | #remove += [filename, filename.chomp('~')] 250 | 251 | files = [] 252 | Dir.chdir(location) do 253 | files += Dir.multiglob_r('**/*') 254 | files -= Dir.multiglob_r(exclusions) 255 | end 256 | return files 257 | end 258 | 259 | # Compute exclusions. 260 | def exclusions 261 | @_exclusions ||= ( 262 | e = [exclude].flatten 263 | e += DEFAULT_EXCLUDE unless all? 264 | e += [filename, filename.chomp('~')] if file 265 | e 266 | ) 267 | end 268 | 269 | # List of files in package, but omit folders. 270 | def files_without_folders 271 | files.select{ |f| !File.directory?(f) } 272 | end 273 | 274 | # Produce textual listing less the manifest file. 275 | 276 | def listing 277 | str = '' 278 | output(str) 279 | str 280 | 281 | # #crypt = (update ? (digest || topline.digest) : digest) 282 | # crypt = digest 283 | # 284 | # list = files #(update) #- [filename, filename.chomp('~')] 285 | # list.sort! 286 | # list.collect!{ |file| [checksum(file,crypt), file] } 287 | # list.collect!{ |file| file.compact.join(' ') } 288 | # list.join("\n") + "\n" 289 | end 290 | 291 | # File content. 292 | 293 | def to_s #(update=false) 294 | topline_string + listing #(update) + listing(update) 295 | end 296 | 297 | private 298 | 299 | # Create a temporary manifest (for comparison). 300 | def save_temporary_manifest 301 | temp_manifest = Manifest.new( 302 | :file => file+"~", 303 | :digest => digest, 304 | :exclude => exclude, 305 | :all => all 306 | ) 307 | temp_manifest.save 308 | #File.open(tempfile, 'w+') do |f| 309 | # f << to_s(true) 310 | #end 311 | return temp_manifest 312 | end 313 | 314 | # Remove temporary manifest. 315 | def remove_temporary_manifest 316 | FileUitls.rm(file+'~') 317 | end 318 | 319 | # Produce hexdigest/cheksum for a file. 320 | # Default digest type is sha1. 321 | def checksum(file, digest=nil) 322 | return nil unless digest 323 | if FileTest.directory?(file) 324 | @null_string ||= digester(digest).hexdigest("") 325 | else 326 | digester(digest).hexdigest(File.read(file)) 327 | end 328 | end 329 | 330 | # Return a digest class for given +type+. 331 | # Supported digests are: 332 | # 333 | # * md5 334 | # * sha1 335 | # * sha128 (same as sha1) 336 | # * sha256 337 | # * sha512 338 | # 339 | # Default digest type is sha256. 340 | def digester(type=nil) 341 | require 'openssl' 342 | case type.to_s.downcase 343 | when 'md5' 344 | require 'digest/md5' 345 | ::Digest::MD5 346 | when 'sha128', 'sha1' 347 | require 'digest/sha1' #need? 348 | OpenSSL::Digest::SHA1 349 | when 'sha256' 350 | require 'digest/sha1' #need? 351 | OpenSSL::Digest::SHA256 352 | when 'sha512' 353 | require 'digest/sha1' #need? 354 | OpenSSL::Digest::SHA512 355 | else 356 | raise "unsupported digest #{type}" 357 | end 358 | end 359 | 360 | # Read manifest file. 361 | def read(file) 362 | return unless file 363 | 364 | #@file = file 365 | #@location = File.dirname(File.expand_path(file)) 366 | 367 | l = {} 368 | flist = File.read_list(file) 369 | flist.each do |line| 370 | left, right = line.split(/\s+/) 371 | if right 372 | checksum = left 373 | filename = right 374 | l[filename] = checksum 375 | else 376 | filename = left 377 | l[filename] = nil 378 | end 379 | end 380 | 381 | a, d, x = *topline_parse 382 | 383 | @list = l 384 | @all = a 385 | @digest = d 386 | @exclude = x 387 | 388 | @read_from_file = file 389 | end 390 | 391 | # Get topline of Manifest file, parse and cache. 392 | #def topline 393 | # @topline ||= topline_parse 394 | #end 395 | 396 | # Parse topline. 397 | # 398 | def topline_parse 399 | if line = read_topline 400 | argv = Shellwords.shellwords(line) 401 | ARGV.replace(argv) 402 | opts = GetoptLong.new( 403 | [ '-g', '--digest' , GetoptLong::REQUIRED_ARGUMENT ], 404 | [ '-x', '--exclude', '--ignore', GetoptLong::REQUIRED_ARGUMENT ], 405 | [ '-a', '--all' , GetoptLong::NO_ARGUMENT ] 406 | ) 407 | a, d, x = false, nil, [] 408 | opts.each do |opt, arg| 409 | case opt 410 | when '-g': d = arg.downcase 411 | when '-a': a = true 412 | when '-x': x << arg 413 | end 414 | end 415 | return a, d, x 416 | end 417 | end 418 | 419 | # Read topline of MANIFEST file. 420 | # 421 | def read_topline 422 | r = nil 423 | #if file = locate(filename) 424 | File.open(file) do |f| 425 | s = f.readline 426 | if s =~ /^#!\s*(.*?)\n/ 427 | r = $1 428 | end 429 | end 430 | return r 431 | #end 432 | end 433 | 434 | # Create topline of MANIFEST file. 435 | # 436 | def topline_string(update=false) 437 | if update 438 | a = all #|| topline.all 439 | d = digest #|| topline.digest 440 | x = exclude #+ topline.exclude 441 | else 442 | a, d, x = all, digest, exclude 443 | end 444 | top = [] 445 | top << "-a" if a 446 | top << "-g #{d.to_s.downcase}" if d 447 | x.each do |e| 448 | top << "-x #{e}" 449 | end 450 | return "#!mast up #{top.join(' ')}\n" # FIXME: use proper executable 451 | end 452 | 453 | end 454 | 455 | end 456 | 457 | 458 | module ProUtils 459 | 460 | # Metaclass extensions for core File class. 461 | module File 462 | 463 | # Is a file a gzip file? 464 | 465 | def gzip?( file ) 466 | open(file,'rb') { |f| 467 | return false unless f.getc == 0x1f 468 | return false unless f.getc == 0x8b 469 | } 470 | true 471 | end 472 | 473 | # Reads in a file, removes blank lines and remarks 474 | # (lines starting with '#') and then returns 475 | # an array of all the remaining lines. 476 | # 477 | # CREDIT: Trans 478 | 479 | def read_list(filepath, chomp_string='') 480 | farr = nil 481 | farr = read(filepath).split("\n") 482 | farr.collect! { |line| 483 | l = line.strip.chomp(chomp_string) 484 | (l.empty? or l[0,1] == '#') ? nil : l 485 | } 486 | farr.compact 487 | end 488 | 489 | end 490 | 491 | # Metaclass extensions for core Dir class. 492 | module Dir 493 | 494 | # Like +glob+ but can take multiple patterns. 495 | # 496 | # Dir.multiglob( '*.rb', '*.py' ) 497 | # 498 | # Rather then constants for options multiglob accepts a trailing options 499 | # hash of symbol keys. 500 | # 501 | # :noescape File::FNM_NOESCAPE 502 | # :casefold File::FNM_CASEFOLD 503 | # :pathname File::FNM_PATHNAME 504 | # :dotmatch File::FNM_DOTMATCH 505 | # :strict File::FNM_PATHNAME && File::FNM_DOTMATCH 506 | # 507 | # It also has an option for recurse. 508 | # 509 | # :recurse Recurively include contents of directories. 510 | # 511 | # For example 512 | # 513 | # Dir.multiglob( '*', :recurse => true ) 514 | # 515 | # would have the same result as 516 | # 517 | # Dir.multiglob('**/*') 518 | # 519 | def multiglob(*patterns) 520 | options = (Hash === patterns.last ? patterns.pop : {}) 521 | 522 | if options.delete(:recurse) 523 | #patterns += patterns.collect{ |f| File.join(f, '**', '**') } 524 | multiglob_r(*patterns) 525 | end 526 | 527 | bitflags = 0 528 | bitflags |= File::FNM_NOESCAPE if options[:noescape] 529 | bitflags |= File::FNM_CASEFOLD if options[:casefold] 530 | bitflags |= File::FNM_PATHNAME if options[:pathname] or options[:strict] 531 | bitflags |= File::FNM_DOTMATCH if options[:dotmatch] or options[:strict] 532 | 533 | patterns = [patterns].flatten.compact 534 | 535 | if options[:recurse] 536 | patterns += patterns.collect{ |f| File.join(f, '**', '**') } 537 | end 538 | 539 | files = [] 540 | files += patterns.collect{ |pattern| Dir.glob(pattern, bitflags) }.flatten.uniq 541 | 542 | return files 543 | end 544 | 545 | # The same as +multiglob+, but recusively includes directories. 546 | # 547 | # Dir.multiglob_r( 'folder' ) 548 | # 549 | # is equivalent to 550 | # 551 | # Dir.multiglob( 'folder', :recurse=>true ) 552 | # 553 | # The effect of which is 554 | # 555 | # Dir.multiglob( 'folder', 'folder/**/**' ) 556 | # 557 | def multiglob_r(*patterns) 558 | options = (Hash === patterns.last ? patterns.pop : {}) 559 | matches = multiglob(*patterns) 560 | directories = matches.select{ |m| File.directory?(m) } 561 | matches += directories.collect{ |d| multiglob_r(File.join(d, '**'), options) }.flatten 562 | matches.uniq 563 | #options = (Hash === patterns.last ? patterns.pop : {}) 564 | #options[:recurse] = true 565 | #patterns << options 566 | #multiglob(*patterns) 567 | end 568 | 569 | end 570 | 571 | end 572 | 573 | class File #:nodoc: 574 | extend ProUtils::File 575 | end 576 | 577 | class Dir #:nodoc: 578 | extend ProUtils::Dir 579 | end 580 | 581 | =begin 582 | # 583 | def manifest_file 584 | apply_naming_policy(@file || DEFAULT_FILE, 'txt') 585 | end 586 | 587 | private 588 | 589 | # Apply naming policy. 590 | # 591 | def apply_naming_policy(name, ext) 592 | return name unless policy 593 | policies = naming_policy.split(' ') 594 | policies.each do |polic| 595 | case polic 596 | when 'downcase' 597 | name = name.downcase 598 | when 'upcase' 599 | name = name.upcase 600 | when 'capitalize' 601 | name = name.capitalize 602 | when 'extension' 603 | name = name + ".#{ext}" 604 | when 'plain' 605 | name = name.chomp(File.extname(name)) 606 | else 607 | name 608 | end 609 | end 610 | return name 611 | end 612 | =end 613 | 614 | #end # module Ratchets 615 | -------------------------------------------------------------------------------- /lib/mast/manifest.rb: -------------------------------------------------------------------------------- 1 | module Mast 2 | require 'fileutils' 3 | require 'getoptlong' 4 | require 'shellwords' 5 | require 'mast/core_ext' 6 | 7 | # TODO: Integrate file signing and general manifest better (?) 8 | # 9 | # TODO: Digester is in sign.rb too. Dry-up? 10 | # 11 | # TODO: The #diff method is shelling out; this needs to be internalized. 12 | # 13 | # TODO: Consider adding @include options rather then scanning entire directory. 14 | # But this can't be done unless we can write a routine that can look at @include 15 | # and reduce it to non-overlapping matches. Eg. [doc, doc/rdoc] should reduce 16 | # to just [doc]. Otherwise we will get duplicate entries, b/c the #output 17 | # method is written for speed and low memory footprint. This might mean @include 18 | # can't use file globs. 19 | 20 | # Manifest stores a list of package files, and optionally checksums. 21 | # 22 | # The class can be used to create and compare package manifests and digests. 23 | # 24 | # Note that the #diff method currently shells out. Eventually this will be 25 | # internalized. 26 | # 27 | class Manifest 28 | 29 | # Manifest file overwrite error. 30 | # 31 | OverwriteError = Class.new(Exception) 32 | 33 | # No Manifest File Error. 34 | # 35 | NoManifestError = Class.new(LoadError) do 36 | def message; "ERROR: no manifest file"; end 37 | end 38 | 39 | # By default mast will exclude any pathname matching 40 | # 'CVS', '_darcs', '.git*' or '.config'. 41 | DEFAULT_EXCLUDE = %w{CVS _darcs .git* .config} # InstalledFiles 42 | 43 | # By default, mast will ignore any file with a name matching 44 | # '.svn' or '*~', ie. ending with a tilde. 45 | DEFAULT_IGNORE = %w{*~ .svn} 46 | 47 | # 48 | DEFAULT_FILE = '{manifest,digest}{,.txt,.list}' 49 | 50 | # Possible file name (was for Fileable?). 51 | #def self.filename 52 | # DEFAULT_FILE 53 | #end 54 | 55 | def self.open(file=nil, options={}) 56 | unless file 57 | file = Dir.glob(filename, File::FNM_CASEFOLD).first 58 | raise NoManifestError, "Manifest file is required." unless file 59 | end 60 | options[:file] = file 61 | new(options) 62 | end 63 | 64 | # Directory of manifest. 65 | attr_accessor :directory 66 | 67 | # File used to store manifest/digest file. 68 | attr_accessor :file 69 | 70 | # Encryption type 71 | attr_accessor :digest 72 | 73 | # Do not exclude standard exclusions. 74 | attr_accessor :all 75 | 76 | # Include directories. 77 | attr_accessor :dir 78 | 79 | # Show as if another manifest (i.e. use file's bang options). 80 | attr_accessor :bang 81 | 82 | # Omit mast header from manifest output. 83 | attr_accessor :headless 84 | 85 | # What files to include. Defaults to ['*']. 86 | # Note that Mast automatically recurses through 87 | # directory entries, so using '**/*' would simply 88 | # be a waste of of processing cycles. 89 | attr_accessor :include 90 | 91 | # What files to exclude. 92 | attr_accessor :exclude 93 | 94 | # Special files to ignore. 95 | attr_accessor :ignore 96 | 97 | # Layout of digest -- 'csf' or 'sfv'. Default is 'csf'. 98 | attr_accessor :format 99 | 100 | # Files and checksums listed in file. 101 | #attr_reader :list 102 | 103 | # An IO object to output manifest. Default is `$stdout`. 104 | attr_accessor :io 105 | 106 | # 107 | alias_method :all?, :all 108 | 109 | # 110 | alias_method :dir?, :dir 111 | 112 | # 113 | alias_method :bang?, :bang 114 | 115 | # 116 | alias_method :headless?, :headless 117 | 118 | 119 | # New Manifest object. 120 | # 121 | def initialize(options={}) 122 | @include = ['*'] 123 | @exclude = [] 124 | @ignore = [] 125 | @format = 'csf' 126 | @all = false 127 | @dir = false 128 | @bang = false 129 | @digest = nil 130 | @directory = Dir.pwd 131 | @io = $stdout 132 | 133 | change_options(options) 134 | 135 | #if @file 136 | # read(@file) 137 | #else 138 | #if file = Dir.glob(self.class.filename)[0] 139 | # @file = file 140 | #else 141 | # @file = DEFAULT_FILE 142 | #end 143 | #end 144 | end 145 | 146 | # Set options. 147 | def change_options(opts) 148 | opts.each do |k,v| 149 | k = k.to_s.downcase 150 | send("#{k}=",v||send(k)) 151 | end 152 | #@file = options[:file] || @file 153 | #@digest = options[:digest] || @digest 154 | #@all = options[:all] || @all 155 | #@exclude = options[:exclude] || options[:ignore] || @exclude 156 | #@exclude = [@exclude].flatten.compact 157 | end 158 | 159 | def include=(inc) 160 | @include = [inc].flatten.uniq 161 | end 162 | 163 | # 164 | def file 165 | @file ||= Dir.glob(File.join(directory, DEFAULT_FILE), File::FNM_CASEFOLD).first || 'MANIFEST' 166 | end 167 | 168 | # 169 | def read? 170 | @read 171 | end 172 | 173 | # 174 | def exist? 175 | file and FileTest.file?(file) 176 | end 177 | 178 | # Is the current mainfest in need of updating? 179 | def changed? 180 | raise NoManifestError unless exist? #file and FileTest.file?(file) 181 | txt = File.read(file) 182 | out = StringIO.new #('', 'w') 183 | generate(out) 184 | out.string != txt 185 | end 186 | 187 | # Create a digest/manifest file. This saves the list of files 188 | # and optionally their checksum. 189 | #def create(options=nil) 190 | # change_options(options) if options 191 | # #@file ||= DEFAULT_FILE 192 | # raise OverwriteError if FileTest.file?(file) 193 | # save #(false) 194 | #end 195 | 196 | # Generate manifest. 197 | def generate(out=nil) 198 | out ||= self.io 199 | parse_topline unless read? if bang? 200 | out << topline_string unless headless? 201 | output(out) 202 | end 203 | 204 | # Update file. 205 | def update 206 | raise NoManifestError unless file and FileTest.file?(file) 207 | parse_topline 208 | save 209 | end 210 | 211 | # 212 | def touch 213 | FileUtils.touch(file) if File.exist?(file) 214 | end 215 | 216 | # Save as file. 217 | def save 218 | File.open(file, 'w') do |f| 219 | f << topline_string 220 | output(f) 221 | end 222 | return file 223 | end 224 | 225 | # Diff file against actual files. 226 | # 227 | # TODO: Do not shell out for diff. 228 | # 229 | def diff 230 | raise NoManifestError unless file and FileTest.file?(file) 231 | parse_topline # parse_file unless read? 232 | manifest = create_temporary_manifest 233 | begin 234 | result = `diff -du #{file} #{manifest.file}` 235 | ensure 236 | FileUtils.rm(manifest.file) 237 | end 238 | # pass = result.empty? 239 | return result 240 | end 241 | 242 | # Files listed in the manifest file, but not found in file system. 243 | # 244 | def whatsold 245 | parse_file unless read? 246 | filelist - list 247 | end 248 | 249 | # Files found in file system, but not listed in the manifest file. 250 | def whatsnew 251 | parse_file unless read? 252 | list - (filelist + [filename]) 253 | end 254 | 255 | # 256 | def verify 257 | parse_file unless read? 258 | chart == filechart 259 | end 260 | 261 | # Clean non-manifest files. 262 | def clean 263 | cfiles, cdirs = cleanlist.partition{ |f| !File.directory?(f) } 264 | if cfiles.empty? && cdirs.empty? 265 | $stderr < "No difference between list and actual.\n" 266 | else 267 | FileUtils.rm(cfiles) 268 | FileUtils.rmdir(cdirs) 269 | end 270 | end 271 | 272 | # List of current files. 273 | def list 274 | @list ||= chart.keys.sort 275 | end 276 | 277 | # Chart of current files (name => checksum). 278 | def chart 279 | @chart ||= parse_directory 280 | end 281 | 282 | # List of files as given in MANIFEST file. 283 | def filelist 284 | @filelist ||= filechart.keys.sort 285 | end 286 | 287 | # Chart of files as given in MANIFEST file (name => checksum). 288 | def filechart 289 | @filechart ||= parse_file 290 | end 291 | 292 | # 293 | def cleanlist 294 | showlist - filelist 295 | end 296 | 297 | # Files not listed in manifest. 298 | def unlisted 299 | list = [] 300 | Dir.chdir(directory) do 301 | list = Dir.glob('**/*') 302 | end 303 | list - filelist 304 | end 305 | 306 | # 307 | def showlist 308 | parse_topline unless read? 309 | list 310 | end 311 | 312 | # # Clobber non-manifest files. 313 | # # 314 | # def clobber 315 | # clobber_files.each{ |f| rm_r(f) if File.exist?(f) } 316 | # end 317 | # 318 | # #-- 319 | # # TODO Should clobber work off the manifest file itself? 320 | # #++ 321 | # def clobber_files 322 | # keep = filelist # + [info.manifest] 323 | # Dir.glob('**/*') - keep 324 | # end 325 | 326 | # File's basename. 327 | def filename 328 | File.basename(file) 329 | end 330 | 331 | private 332 | 333 | # 334 | def output(out=nil) 335 | out ||= self.io 336 | Dir.chdir(directory) do 337 | exclusions # seed exclusions 338 | #rec_output('*', out) 339 | inclusions.each do |inc| 340 | rec_output(inc, out) 341 | end 342 | end 343 | end 344 | 345 | # Generate listing on the fly. 346 | def rec_output(match, out=nil) 347 | out ||= self.io 348 | out.flush unless Array === out 349 | #match = (location == dir ? '*' : File.join(dir,'*')) 350 | files = Dir.glob(match, File::FNM_DOTMATCH) - exclusions 351 | # TODO: Is there a more efficient way to reject ignored files? 352 | #files = files.select{ |f| !ignores.any?{ |i| File.fnmatch(i, File.basename(f)) } } 353 | files = files.reject{ |f| ignores.any?{ |i| File.fnmatch(i, File.basename(f)) } } 354 | files = files.sort 355 | files.each do |file| 356 | is_dir = File.directory?(file) 357 | if !is_dir || (is_dir && dir?) 358 | sum = checksum(file,digest) 359 | sum = sum + ' ' if sum 360 | out << "#{sum}#{file}" 361 | out << "\n" unless Array === out 362 | end 363 | if is_dir 364 | rec_output(File.join(file,'*'), out) 365 | end 366 | end 367 | #return out 368 | end 369 | 370 | # 371 | def parse_directory 372 | h = {} 373 | files.each do |f| 374 | h[f] = checksum(f) 375 | end 376 | h 377 | end 378 | 379 | # List of files. 380 | # 381 | def files #(update=false) 382 | @files ||= ( 383 | r = [] 384 | output(r) 385 | r 386 | ) 387 | #files = [] 388 | #Dir.chdir(directory) do 389 | # files += Dir.multiglob_r('**/*') 390 | # files -= Dir.multiglob_r(exclusions) 391 | #end 392 | #return files 393 | end 394 | 395 | # Compute exclusions. 396 | def inclusions 397 | @_inclusions ||= ( 398 | e = [include].flatten 399 | #e += DEFAULT_EXCLUDE unless all? 400 | #e += [filename, filename.chomp('~')] if file 401 | e = e.map{ |x| Dir.glob(x) }.flatten.uniq 402 | e = File.reduce(*e) 403 | e 404 | ) 405 | end 406 | 407 | # Compute exclusions. 408 | def exclusions 409 | @_exclusions ||= ( 410 | e = [exclude].flatten 411 | e += DEFAULT_EXCLUDE unless all? 412 | e += [filename, filename.chomp('~')] if file 413 | e.map{ |x| Dir.glob(x) }.flatten.uniq 414 | ) 415 | end 416 | 417 | # Compute ignores. 418 | def ignores 419 | @_ignores ||= ( 420 | i = [ignore].flatten 421 | i += [ '.', '..' ] 422 | i += DEFAULT_IGNORE unless all? 423 | i 424 | ) 425 | end 426 | 427 | public 428 | 429 | # List of files in file system, but omit folders. 430 | def list_without_folders 431 | list.select{ |f| !File.directory?(f) } 432 | end 433 | 434 | # Produce textual listing less the manifest file. 435 | # 436 | def listing 437 | str = '' 438 | output(str) 439 | str 440 | end 441 | 442 | # 443 | def to_s 444 | topline_string + listing 445 | end 446 | 447 | private 448 | 449 | # Create temporary manifest (for comparison). 450 | 451 | def create_temporary_manifest 452 | temp_manifest = Manifest.new( 453 | :file => file+"~", 454 | :digest => digest, 455 | :include => include, 456 | :exclude => exclude, 457 | :ignore => ignore, 458 | :all => all 459 | ) 460 | temp_manifest.save 461 | #File.open(tempfile, 'w+') do |f| 462 | # f << to_s(true) 463 | #end 464 | return temp_manifest 465 | end 466 | 467 | # Produce hexdigest/cheksum for a file. 468 | # Default digest type is sha1. 469 | 470 | def checksum(file, digest=nil) 471 | return nil unless digest 472 | if FileTest.directory?(file) 473 | #@null_string ||= digester(digest).hexdigest("") 474 | listing = (Dir.entries(file) - %w{. ..}).join("\n") 475 | digester(digest).hexdigest(listing) 476 | else 477 | digester(digest).hexdigest(File.read(file)) 478 | end 479 | end 480 | 481 | # Return a digest class for given +type+. 482 | # Supported digests are: 483 | # 484 | # * md5 485 | # * sha1 486 | # * sha128 (same as sha1) 487 | # * sha256 488 | # * sha512 489 | # 490 | # Default digest type is sha256. 491 | 492 | def digester(type=nil) 493 | require 'openssl' 494 | case type.to_s.downcase 495 | when 'md5' 496 | require 'digest/md5' 497 | ::Digest::MD5 498 | when 'sha128', 'sha1' 499 | require 'digest/sha1' #need? 500 | OpenSSL::Digest::SHA1 501 | when 'sha256' 502 | require 'digest/sha1' #need? 503 | OpenSSL::Digest::SHA256 504 | when 'sha512' 505 | require 'digest/sha1' #need? 506 | OpenSSL::Digest::SHA512 507 | else 508 | raise "unsupported digest #{type}" 509 | end 510 | end 511 | 512 | # Read manifest file. 513 | 514 | def parse_file 515 | raise ManifestMissing unless file 516 | 517 | parse_topline 518 | 519 | #@file = file 520 | #@location = File.dirname(File.expand_path(file)) 521 | 522 | chart = {} 523 | flist = File.read_list(file) 524 | flist.each do |line| 525 | left, right = line.split(/\s+/) 526 | if right 527 | checksum = left 528 | filename = right 529 | chart[filename] = checksum 530 | else 531 | filename = left 532 | chart[filename] = nil 533 | end 534 | end 535 | 536 | @read = true 537 | @filechart = chart 538 | end 539 | 540 | # Get topline of Manifest file, parse and cache. 541 | #def topline 542 | # @topline ||= topline_parse 543 | #end 544 | 545 | # Parse topline. 546 | # 547 | def parse_topline 548 | if line = read_topline 549 | argv = Shellwords.shellwords(line) 550 | ARGV.replace(argv) 551 | opts = GetoptLong.new( 552 | [ '-g', '--digest' , GetoptLong::REQUIRED_ARGUMENT ], 553 | [ '-x', '--exclude', GetoptLong::REQUIRED_ARGUMENT ], 554 | [ '-i', '--ignore' , GetoptLong::REQUIRED_ARGUMENT ], 555 | [ '-a', '--all' , GetoptLong::NO_ARGUMENT ] 556 | ) 557 | a, d, g, x, i = false, false, nil, [], [] 558 | opts.each do |opt, arg| 559 | case opt 560 | when '-g' 561 | g = arg.downcase 562 | when '-a' 563 | a = true 564 | when '-x' 565 | x << arg 566 | when '-i' 567 | i << arg 568 | when '-d' 569 | d = true 570 | end 571 | end 572 | 573 | @all = a 574 | @digest = g 575 | @exclude = x 576 | @ignore = i 577 | @dir = d 578 | @include = ARGV.empty? ? nil : ARGV.dup 579 | end 580 | end 581 | 582 | # Read topline of MANIFEST file. 583 | # 584 | def read_topline 585 | r = nil 586 | #if file = locate(filename) 587 | File.open(file) do |f| 588 | s = f.readline 589 | if s =~ /^#!mast\s*(.*?)\n/ 590 | r = $1 591 | end 592 | end 593 | return r 594 | #end 595 | end 596 | 597 | # Create topline of MANIFEST file. 598 | # 599 | def topline_string(update=false) 600 | #if update 601 | # a = all #|| topline.all 602 | # d = digest #|| topline.digest 603 | # x = exclude #+ topline.exclude 604 | #else 605 | # a, d, x = all, digest, exclude 606 | #end 607 | top = [] 608 | top << "-a" if all? 609 | top << "-d" if dir? 610 | top << "-g #{digest.to_s.downcase}" if digest 611 | exclude.each do |e| 612 | top << "-x #{e}" 613 | end 614 | ignore.each do |e| 615 | top << "-i #{e}" 616 | end 617 | include.each do |e| 618 | top << e 619 | end 620 | return "#!mast #{top.join(' ')}\n" # FIXME: use proper executable 621 | end 622 | 623 | end 624 | 625 | end 626 | 627 | 628 | =begin 629 | # 630 | def manifest_file 631 | apply_naming_policy(@file || DEFAULT_FILE, 'txt') 632 | end 633 | 634 | private 635 | 636 | # Apply naming policy. 637 | # 638 | def apply_naming_policy(name, ext) 639 | return name unless policy 640 | policies = naming_policy.split(' ') 641 | policies.each do |polic| 642 | case polic 643 | when 'downcase' 644 | name = name.downcase 645 | when 'upcase' 646 | name = name.upcase 647 | when 'capitalize' 648 | name = name.capitalize 649 | when 'extension' 650 | name = name + ".#{ext}" 651 | when 'plain' 652 | name = name.chomp(File.extname(name)) 653 | else 654 | name 655 | end 656 | end 657 | return name 658 | end 659 | =end 660 | 661 | #end # module Ratchets 662 | --------------------------------------------------------------------------------