fixi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ *.swp
3
+ .fixi
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fixi.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/fixi ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'trollop'
5
+ require 'fixi'
6
+
7
+ name = ARGV.shift
8
+ if name == "--help" || name == "-h" || name == "help"
9
+ name = ARGV.shift
10
+ if name.nil?
11
+ puts <<-EOS
12
+ usage: fixi [--version] [--help] <command> [<options>] [path]
13
+
14
+ All commands are scoped to the current directory or the given path, if specified.
15
+
16
+ Commands:
17
+ add: #{Fixi::Command::Add.synopsis}
18
+ check: #{Fixi::Command::Check.synopsis}
19
+ commit: #{Fixi::Command::Commit.synopsis}
20
+ info: #{Fixi::Command::Info.synopsis}
21
+ init: #{Fixi::Command::Init.synopsis}
22
+ ls: #{Fixi::Command::Ls.synopsis}
23
+ rm: #{Fixi::Command::Rm.synopsis}
24
+ sum: #{Fixi::Command::Sum.synopsis}
25
+
26
+ See 'fixi help <command>' for more information on a specific command.
27
+ EOS
28
+ exit 0
29
+ else
30
+ ARGV.insert(0, "--help")
31
+ end
32
+ elsif name == "--version" || name == "-v"
33
+ puts "fixi version #{Fixi::VERSION}"
34
+ exit 0
35
+ end
36
+
37
+ command = Fixi::command name
38
+ if command.nil?
39
+ puts "Error: No such command: #{name}"
40
+ exit 1
41
+ end
42
+ begin
43
+ command.execute ARGV
44
+ rescue RuntimeError => msg
45
+ puts "Error: #{msg}"
46
+ exit 1
47
+ #rescue => msg
48
+ # puts "Error: #{msg}"
49
+ # exit 1
50
+ end
data/fixi.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "fixi/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "fixi"
7
+ s.version = Fixi::VERSION
8
+ s.authors = ["Chris Wilper"]
9
+ s.email = ["cwilper@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = "A fixity tracker utility"
12
+ s.description = "Keeps an index of checksums and lets you update and verify them"
13
+
14
+ s.rubyforge_project = "fixi"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ # s.add_development_dependency "rspec"
23
+ # s.add_runtime_dependency "rest-client"
24
+
25
+ s.add_runtime_dependency "trollop"
26
+ end
@@ -0,0 +1,33 @@
1
+ require 'trollop'
2
+ require 'fixi/index'
3
+
4
+ class Fixi::Command::Add
5
+ def self.synopsis
6
+ "Add new files to the index"
7
+ end
8
+
9
+ def self.details
10
+ "This command is scoped to the current directory or the given path,
11
+ if specified.".pack
12
+ end
13
+
14
+ def execute args
15
+ opts = Trollop::options args do
16
+ banner Fixi::Command.banner "add"
17
+ opt :absolute, "Show absolute paths. By default, paths are reported
18
+ relative to the index root.".pack
19
+ opt :dry_run, "Don't do anything; just report what would be done"
20
+ end
21
+ path = File.expand_path(args[0] || ".")
22
+ index = Fixi::Index.new(path)
23
+
24
+ index.find(path) do |abspath|
25
+ relpath = index.relpath(abspath)
26
+ unless index.contains?(relpath)
27
+ puts "A #{opts[:absolute] ? abspath : relpath}"
28
+ index.add(relpath) unless opts[:dry_run]
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,66 @@
1
+ require 'trollop'
2
+
3
+ class Fixi::Command::Check
4
+ def self.synopsis
5
+ "Verify the fixity of files in the index"
6
+ end
7
+
8
+ def self.details
9
+ "This command is scoped to the current directory or the given path,
10
+ if specified.".pack
11
+ end
12
+
13
+ def execute args
14
+ opts = Trollop::options args do
15
+ banner Fixi::Command.banner "check"
16
+ opt :absolute, "Show absolute paths. By default, paths are reported
17
+ relative to the index root.".pack
18
+ opt :shallow, "Do shallow comparisons when determining which files have
19
+ changed. If specified, only file sizes and mtimes will be used. By
20
+ default, checksums will also be computed and compared if necessary.".pack
21
+ opt :verbose, "For modified files, show which attribute changed.
22
+ By default, only the path is shown.".pack
23
+ end
24
+ path = File.expand_path(args[0] || ".")
25
+ index = Fixi::Index.new(path)
26
+
27
+ index.each(args[0]) do |hash|
28
+ relpath = hash['relpath']
29
+ abspath = index.rootpath + '/' + relpath
30
+ if index.file_in_scope(relpath)
31
+ if File.exists?(abspath)
32
+ size = File.size(abspath)
33
+ mtime = File.mtime(abspath).to_i
34
+ if size != hash['size']
35
+ detail = opts[:verbose] ? "size=#{size} " : ""
36
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
37
+ elsif File.mtime(abspath).to_i != hash['mtime']
38
+ detail = opts[:verbose] ? "mtime=#{Time.at(mtime).utc.iso8601} " : ""
39
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
40
+ elsif not opts[:shallow]
41
+ hexdigests = Fixi::hexdigests(Fixi::digests(index.algorithms), abspath)
42
+ i = 0
43
+ index.algorithms.split(',').each do |algorithm|
44
+ if hexdigests[i] != hash[algorithm]
45
+ detail = opts[:verbose] ? "#{algorithm}=#{hexdigests[i]} " : ""
46
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
47
+ end
48
+ i += 1
49
+ end
50
+ end
51
+ else
52
+ puts "D #{opts[:absolute] ? abspath : relpath}"
53
+ end
54
+ else
55
+ puts "X #{opts[:absolute] ? abspath : relpath}"
56
+ end
57
+ end
58
+
59
+ index.find(path) do |abspath|
60
+ relpath = index.relpath(abspath)
61
+ unless index.contains?(relpath)
62
+ puts "A #{opts[:absolute] ? abspath : relpath}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,62 @@
1
+ require 'trollop'
2
+ require 'find'
3
+ require 'fixi/index'
4
+
5
+ class Fixi::Command::Commit
6
+ def self.synopsis
7
+ "Commit modified files to the index"
8
+ end
9
+
10
+ def self.details
11
+ "This command is scoped to the current directory or the given path,
12
+ if specified.".pack
13
+ end
14
+
15
+ def execute args
16
+ opts = Trollop::options args do
17
+ banner Fixi::Command.banner "commit"
18
+ opt :absolute, "Show absolute paths. By default, paths are reported
19
+ relative to the index root.".pack
20
+ opt :dry_run, "Don't do anything; just report what would be done"
21
+ opt :shallow, "Do shallow comparisons when determining which files have
22
+ changed. If specified, only file sizes and mtimes will be used. By
23
+ default, checksums will also be computed and compared if necessary.".pack
24
+ opt :verbose, "For modified files, show which attribute changed.
25
+ By default, only the path is shown.".pack
26
+ end
27
+ path = File.expand_path(args[0] || ".")
28
+ index = Fixi::Index.new(path)
29
+
30
+ index.each(args[0]) do |hash|
31
+ relpath = hash['relpath']
32
+ abspath = index.rootpath + '/' + relpath
33
+ if index.file_in_scope(relpath) && File.exists?(abspath)
34
+ size = File.size(abspath)
35
+ mtime = File.mtime(abspath).to_i
36
+ if size != hash['size']
37
+ detail = opts[:verbose] ? "size=#{size} " : ""
38
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
39
+ index.update(relpath) unless opts[:dry_run]
40
+ elsif mtime != hash['mtime']
41
+ detail = opts[:verbose] ? "mtime=#{Time.at(mtime).utc.iso8601} " : ""
42
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
43
+ index.update(relpath) unless opts[:dry_run]
44
+ elsif not opts[:shallow]
45
+ hexdigests = Fixi::hexdigests(Fixi::digests(index.algorithms), abspath)
46
+ i = 0
47
+ need_update = false
48
+ index.algorithms.split(',').each do |algorithm|
49
+ if not(need_update) && (hexdigests[i] != hash[algorithm])
50
+ need_update = true
51
+ detail = opts[:verbose] ? "#{algorithm}=#{hexdigests[i]} " : ""
52
+ puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
53
+ end
54
+ hash[algorithm] = hexdigests[i]
55
+ i += 1
56
+ end
57
+ index.update(relpath, hash) if need_update && not(opts[:dry_run])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ require 'trollop'
2
+ require 'fixi/index'
3
+
4
+ class Fixi::Command::Info
5
+ def self.synopsis
6
+ "Display a summary of the index"
7
+ end
8
+
9
+ def self.details
10
+ "This command is scoped to the current directory or the given path,
11
+ if specified.".pack
12
+ end
13
+
14
+ def execute args
15
+ opts = Trollop::options args do
16
+ banner Fixi::Command::banner "info"
17
+ end
18
+ Fixi::Index.new(args[0]).describe
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ require 'trollop'
2
+ require 'fixi/index'
3
+
4
+ class Fixi::Command::Init
5
+ def self.synopsis
6
+ "Create a new, empty index"
7
+ end
8
+
9
+ def self.details
10
+ "This command is scoped to the current directory or the given path,
11
+ if specified.".pack
12
+ end
13
+
14
+ def execute args
15
+ opts = Trollop::options args do
16
+ banner Fixi::Command::banner "init"
17
+ opt :algorithms, "Checksum algorithm(s) to use for the index. This is
18
+ a comma-separated list, which may include md5, sha1, sha256, sha384, and
19
+ sha512.".pack, :default => "sha256", :short => 'l'
20
+ end
21
+ index = Fixi::Index.new(args[0], true, opts[:algorithms])
22
+ puts "Initialized empty index at #{index.dotpath}"
23
+ end
24
+ end
@@ -0,0 +1,70 @@
1
+ require 'time'
2
+ require 'trollop'
3
+
4
+ class Fixi::Command::Ls
5
+ def self.synopsis
6
+ "List contents of the index"
7
+ end
8
+
9
+ def self.details
10
+ "This command is scoped to the current directory or the given path,
11
+ if specified.".pack
12
+ end
13
+
14
+ def execute args
15
+ opts = Trollop::options args do
16
+ banner Fixi::Command.banner "ls"
17
+ opt :absolute, "Show absolute paths. By default, paths are reported
18
+ relative to the index root.".pack
19
+ opt :json, "Like --verbose, but outputs the result as a json array."
20
+ opt :md5, "Restrict list to files with the given md5 checksum",
21
+ :type => :string, :short => :none
22
+ opt :sha1, "Restrict list to files with the given sha1 checksum",
23
+ :type => :string, :short => :none
24
+ opt :sha256, "Restrict list to files with the given sha256 checksum",
25
+ :type => :string, :short => :none
26
+ opt :sha384, "Restrict list to files with the given sha384 checksum",
27
+ :type => :string, :short => :none
28
+ opt :sha512, "Restrict list to files with the given sha512 checksum",
29
+ :type => :string, :short => :none
30
+ opt :verbose, "Include all information known about each file. By default,
31
+ only paths will be listed.".pack
32
+ end
33
+ index = Fixi::Index.new(args[0])
34
+ if opts[:json]
35
+ print "["
36
+ end
37
+ i = 0
38
+ index.each(args[0], opts) do |hash|
39
+ path = hash['relpath']
40
+ path = index.rootpath + '/' + path if opts[:absolute]
41
+ if opts[:verbose]
42
+ print "size=#{hash['size']},mtime=#{Time.at(hash['mtime']).utc.iso8601}"
43
+ print ",md5=#{hash['md5']}" if hash['md5']
44
+ print ",sha1=#{hash['sha1']}" if hash['sha1']
45
+ print ",sha256=#{hash['sha256']}" if hash['sha256']
46
+ print ",sha384=#{hash['sha384']}" if hash['sha384']
47
+ print ",sha512=#{hash['sha512']}" if hash['sha512']
48
+ puts " #{path}"
49
+ elsif opts[:json]
50
+ print "," if i > 0
51
+ puts "\n { path: \"#{path}\","
52
+ puts " size: \"#{hash['size']}\","
53
+ print " mtime: \"#{Time.at(hash['mtime']).utc.iso8601}\""
54
+ print ",\n md5: \"#{hash['md5']}\"" if hash['md5']
55
+ print ",\n sha1: \"#{hash['sha1']}\"" if hash['md5']
56
+ print ",\n sha256: \"#{hash['sha256']}\"" if hash['sha256']
57
+ print ",\n sha384: \"#{hash['sha384']}\"" if hash['sha384']
58
+ print ",\n sha512: \"#{hash['sha512']}\"" if hash['sha512']
59
+ print " }"
60
+ else
61
+ puts path
62
+ end
63
+ i += 1
64
+ end
65
+ if opts[:json]
66
+ print "\n" if i > 0
67
+ puts "]"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ require 'trollop'
2
+ require 'find'
3
+ require 'fixi/index'
4
+
5
+ class Fixi::Command::Rm
6
+ def self.synopsis
7
+ "Delete old files from the index"
8
+ end
9
+
10
+ def self.details
11
+ "This command is scoped to the current directory or the given path,
12
+ if specified.".pack
13
+ end
14
+
15
+ def execute args
16
+ opts = Trollop::options args do
17
+ banner Fixi::Command.banner "rm"
18
+ opt :absolute, "Show absolute paths. By default, paths are reported
19
+ relative to the index root.".pack
20
+ opt :dry_run, "Don't do anything; just report what would be done"
21
+ end
22
+ path = File.expand_path(args[0] || ".")
23
+ index = Fixi::Index.new(path)
24
+
25
+ index.each(args[0]) do |hash|
26
+ relpath = hash['relpath']
27
+ abspath = index.rootpath + '/' + relpath
28
+ if index.file_in_scope(relpath)
29
+ unless File.exists?(abspath)
30
+ puts "D #{opts[:absolute] ? abspath : relpath}"
31
+ index.delete relpath unless opts[:dry_run]
32
+ end
33
+ else
34
+ puts "X #{opts[:absolute] ? abspath : relpath}"
35
+ index.delete relpath unless opts[:dry_run]
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ require 'trollop'
2
+ require 'find'
3
+ require 'fixi/index'
4
+
5
+ class Fixi::Command::Sum
6
+ def self.synopsis
7
+ "Calculate checksum(s) of a file"
8
+ end
9
+
10
+ def self.details
11
+ "This command operates on files and does not require an index to exist."
12
+ end
13
+
14
+ def execute args
15
+ opts = Trollop::options args do
16
+ banner Fixi::Command.banner "sum"
17
+ opt :algorithms, "Checksum algorithm(s) to use. This is a comma-separated
18
+ list, which may include md5, sha1, sha256, sha384, and sha512. At least
19
+ one must be specified.".pack, :short => 'l', :type => :string,
20
+ :required => true
21
+ end
22
+ unless args[0]
23
+ raise "Must specify a file."
24
+ exit 1
25
+ end
26
+ path = args[0]
27
+ unless File.exists?(path)
28
+ raise "No such file: #{path}"
29
+ exit 1
30
+ end
31
+ hexdigests = Fixi::hexdigests(Fixi::digests(opts[:algorithms]), path)
32
+ hexdigests.each { |hexdigest| puts "#{hexdigest}" }
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Fixi::Command
2
+ def self.banner(name)
3
+ "fixi-#{name}: #{const_get(name.capitalize).synopsis}\n\n" +
4
+ "usage: fixi #{name} [<options>] [path]\n\n" +
5
+ "#{const_get(name.capitalize).details}\n\n" +
6
+ "Options:"
7
+ end
8
+ end
9
+
10
+ Dir.glob(File.dirname(__FILE__) + '/command/*') {|file| require file}
data/lib/fixi/index.rb ADDED
@@ -0,0 +1,203 @@
1
+ require 'sqlite3'
2
+
3
+ class Fixi::Index
4
+ attr_reader :dotpath, :rootpath, :dbversion, :algorithms
5
+
6
+ def initialize(startpath, create=false, algorithms=nil)
7
+ startpath = File.expand_path(startpath || ".")
8
+ unless File.directory?(startpath)
9
+ raise "No such file or directory: #{startpath}" unless File.exist?(startpath)
10
+ startpath = File.dirname(startpath)
11
+ end
12
+ if create
13
+ Fixi::digests(algorithms)
14
+ @dotpath = File.join(startpath, ".fixi")
15
+ raise "Index already exists at #{@dotpath}" if Dir.exist? @dotpath
16
+ Dir.mkdir @dotpath
17
+ @db = SQLite3::Database.new(File.join(@dotpath, "fixi.db"))
18
+ @dbversion = 1
19
+ @algorithms = algorithms
20
+ ddl = <<-EOS
21
+ create table fixi (
22
+ dbversion text,
23
+ algorithms text
24
+ );
25
+ create table file (
26
+ relpath text primary key,
27
+ size integer not null,
28
+ mtime integer not null,
29
+ md5 text,
30
+ sha1 text,
31
+ sha256 text,
32
+ sha384 text,
33
+ sha512 text
34
+ );
35
+ insert into fixi (dbversion, algorithms)
36
+ values (#{@dbversion}, "#{@algorithms}");
37
+ EOS
38
+ @db.execute_batch ddl
39
+ open(File.join(@dotpath, "includes"), "w") { |f| f.puts ".*" }
40
+ open(File.join(@dotpath, "excludes"), "w") { |f| f.puts "^\\.fixi\\/" }
41
+ else
42
+ @dotpath = find_dotpath(startpath)
43
+ @db = SQLite3::Database.new(File.join(@dotpath, "fixi.db"))
44
+ @db.execute("select dbversion, algorithms from fixi") do |row|
45
+ @dbversion = row[0]
46
+ @algorithms = row[1]
47
+ end
48
+ end
49
+ @includes = load_patterns("includes")
50
+ @excludes = load_patterns("excludes")
51
+ @rootpath = File.expand_path(File.join(@dotpath, ".."))
52
+ @db.results_as_hash = true
53
+ end
54
+
55
+ # Traverse the given path within @rootdir and return all files
56
+ # of interest (those matching @includes and not matching @excludes)
57
+ def find(path)
58
+ Find.find(path) do |abspath|
59
+ relpath = relpath(abspath)
60
+ if relpath.length > 0
61
+ if matches_any?(relpath, @includes)
62
+ if matches_any?(relpath, @excludes)
63
+ Find.prune
64
+ else
65
+ yield abspath unless File.directory?(abspath)
66
+ end
67
+ else
68
+ Find.prune unless File.directory?(abspath)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def file_in_scope(relpath)
75
+ return matches_any?(relpath, @includes) &&
76
+ not(matches_any?(relpath, @excludes))
77
+ end
78
+
79
+ def each(path=nil, attribs={})
80
+ sql = "select * from file"
81
+ conditions = []
82
+ if path && File.expand_path(path) != @rootpath
83
+ relpath = relpath(File.expand_path(path))
84
+ if File.directory?(path)
85
+ conditions << " relpath like '#{relpath}/%'"
86
+ else
87
+ conditions << " relpath = '#{relpath}'"
88
+ end
89
+ end
90
+ attribs.each do |name,value|
91
+ if value && (name == :size || name == :mtime ||
92
+ name == :md5 || name == :sha1 || name == :sha256 ||
93
+ name == :sha384 || name == :sha512)
94
+ c = "#{name} = "
95
+ c += value.is_a?(Numeric) ? "#{value}" : "'#{value}'"
96
+ conditions << c
97
+ end
98
+ end
99
+ unless conditions.size == 0
100
+ sql += " where"
101
+ conditions.each do |c|
102
+ sql += " and" unless c == conditions.first
103
+ sql += " #{c}"
104
+ end
105
+ end
106
+ @db.execute(sql) do |hash|
107
+ yield hash
108
+ end
109
+ end
110
+
111
+ def size
112
+ @db.get_first_value("select count(*) from file")
113
+ end
114
+
115
+ def contains?(relpath)
116
+ @db.get_first_value("select count(*) from file where relpath = ?", relpath) > 0
117
+ end
118
+
119
+ def relpath(abspath)
120
+ return "" if abspath == @rootpath
121
+ abspath.slice(@rootpath.length + 1..-1)
122
+ end
123
+
124
+ def update(relpath, hash=nil)
125
+ abspath = File.join(@rootpath, relpath)
126
+ unless hash
127
+ hash = Hash.new
128
+ hash['size'] = File.size(abspath)
129
+ hash['mtime'] = File.mtime(abspath).to_i
130
+ hexdigests = Fixi::hexdigests(Fixi::digests(@algorithms), abspath)
131
+ i = 0
132
+ @algorithms.split(',').each do |algorithm|
133
+ hash[algorithm] = hexdigests[i]
134
+ end
135
+ end
136
+ sql = "update file set size = #{hash['size']}, mtime = #{hash['mtime']}"
137
+ @algorithms.split(',').each do |algorithm|
138
+ sql += ", #{algorithm} = '#{hash[algorithm]}'"
139
+ end
140
+ sql += " where relpath = ?"
141
+ @db.execute(sql, relpath)
142
+ end
143
+
144
+ def add(relpath)
145
+ abspath = File.join(@rootpath, relpath)
146
+ sql = "insert into file (relpath, size, mtime, "
147
+ sql += @algorithms
148
+ sql += ") values (:relpath, :size, :mtime, "
149
+ values = Hash.new
150
+ values[:relpath] = relpath
151
+ values[:size] = File.size abspath
152
+ values[:mtime] = File.mtime(abspath).to_i
153
+ hexdigests = Fixi::hexdigests(Fixi::digests(@algorithms), abspath)
154
+ i = 0
155
+ @algorithms.split(",").each do |alg|
156
+ sql += ", " if i > 0
157
+ sql += "'" + hexdigests[i] + "'"
158
+ i += 1
159
+ end
160
+ sql += ")"
161
+ @db.execute(sql, values)
162
+ end
163
+
164
+ def delete(relpath)
165
+ @db.execute("delete from file where relpath = ?", relpath)
166
+ end
167
+
168
+ def describe
169
+ puts "#{size} files indexed at #{@dotpath}"
170
+ puts "Using checksum algorithm(s) [#{@algorithms}]"
171
+ puts "Fixi database version #{dbversion}"
172
+ end
173
+
174
+ private
175
+
176
+ def load_patterns(name)
177
+ result = []
178
+ f = File.join(@dotpath, name)
179
+ if File.exists? f
180
+ File.foreach(f) do |line|
181
+ line = line.chomp
182
+ result << Regexp.new(line) if line.length > 0
183
+ end
184
+ end
185
+ result
186
+ end
187
+
188
+ def matches_any?(path, patterns)
189
+ patterns.each do |pattern|
190
+ return true if pattern.match(path)
191
+ end
192
+ return false
193
+ end
194
+
195
+ # Return the first .fixi directory we find while traversing up the tree
196
+ def find_dotpath(path, startpath=path)
197
+ dotpath = File.join(path, ".fixi")
198
+ return dotpath if Dir.exist?(dotpath)
199
+ parent = File.dirname(path)
200
+ return find_dotpath(parent, startpath) unless parent == path
201
+ raise "No index at #{startpath} or any parent"
202
+ end
203
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def pack
3
+ self.gsub(/\s+/, ' ').strip
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Fixi
2
+ VERSION = "0.0.1"
3
+ end
data/lib/fixi.rb ADDED
@@ -0,0 +1,42 @@
1
+ require "digest"
2
+ require "fixi/version"
3
+ require "fixi/patch/string_pack"
4
+ require "fixi/command"
5
+
6
+ module Fixi
7
+ # Get an instance of the command with the given name
8
+ def self.command(name)
9
+ return nil unless Command.const_defined? name.capitalize
10
+ Command.const_get(name.capitalize).new
11
+ end
12
+
13
+ # Validate the given comma-separated list of checksum algorithms
14
+ # and return and array of matching Digest implementations
15
+ def self.digests(checksums)
16
+ digests = []
17
+ checksums.split(",").each do |checksum|
18
+ begin
19
+ digests << Digest(checksum.upcase).new
20
+ rescue LoadError
21
+ raise "No such algorithm: #{checksum}"
22
+ end
23
+ end
24
+ digests
25
+ end
26
+
27
+ # Read the file once while computing any number of digests
28
+ def self.hexdigests(digests, file)
29
+ File.open(file, "rb") {|f|
30
+ buf = ""
31
+ while f.read(16384, buf)
32
+ digests.each {|digest| digest.update buf}
33
+ end
34
+ }
35
+ hds = []
36
+ digests.each {|digest|
37
+ hd = digest.hexdigest
38
+ hds << hd
39
+ }
40
+ hds
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixi
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Chris Wilper
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-12-08 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: trollop
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description: Keeps an index of checksums and lets you update and verify them
27
+ email:
28
+ - cwilper@gmail.com
29
+ executables:
30
+ - fixi
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - .gitignore
37
+ - Gemfile
38
+ - Rakefile
39
+ - bin/fixi
40
+ - fixi.gemspec
41
+ - lib/fixi.rb
42
+ - lib/fixi/command.rb
43
+ - lib/fixi/command/add.rb
44
+ - lib/fixi/command/check.rb
45
+ - lib/fixi/command/commit.rb
46
+ - lib/fixi/command/info.rb
47
+ - lib/fixi/command/init.rb
48
+ - lib/fixi/command/ls.rb
49
+ - lib/fixi/command/rm.rb
50
+ - lib/fixi/command/sum.rb
51
+ - lib/fixi/index.rb
52
+ - lib/fixi/patch/string_pack.rb
53
+ - lib/fixi/version.rb
54
+ homepage: ""
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ requirements: []
75
+
76
+ rubyforge_project: fixi
77
+ rubygems_version: 1.8.11
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: A fixity tracker utility
81
+ test_files: []
82
+