geordi 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c07036b9657ebe6dfccf09dbc5c74e9b449b1eed88fc04ca5e880284dcbdc1e7
4
- data.tar.gz: 1bf48b1318b06182ccfe8bc0e294c7344195ed654e28c983f6c44bdea0bde7cf
3
+ metadata.gz: 70f9242df556d6b8259f341827cc8996f6970de1e9d51c5ff13cbc44307a7ee7
4
+ data.tar.gz: 59e54eca6438f61af0c7640f88771e5f7f67008237445a0d25521b3adea4f0fd
5
5
  SHA512:
6
- metadata.gz: ae4590a4eb870deec8055b6e4cea3e433b85dc202b94be155fa78d2b8ce8f05b9126c73c2151b04ccf1d09fd5cedf2e80b2f49b80b3efabba61fe551cf5e92e2
7
- data.tar.gz: 2829290660f89550294c2f83423bfa5608533f6f952b951def2ccf39fec167a5146dfe9d84b94eb40f6707085d7a1ba27cc81b0014892d86092e1c9eb05f8602
6
+ metadata.gz: d143f1c68d472cc9918d6de6d2271b6633d0f9f01182d835b9667d872d65c495cc85b6c2752545e670b4bc685367baeb7122a32b7e4d3265ea8cc6412629be5d
7
+ data.tar.gz: ec93583e23658f4c1c69febc903f8d3d05c98b6e0e7ec7066c4c678277161fcc9d4e8f365e79e38792ee0c7a5b8bb9deec07408983c50c694a3884be254d2124
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- geordi (1.7.1)
4
+ geordi (1.9.0)
5
5
  thor (>= 0.18.0)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -75,6 +75,18 @@ to use an `=` instead of a space to separate parameter name and value,
75
75
  e.g. `--format=pretty`.
76
76
 
77
77
 
78
+ ### geordi delete_dumps [DIRECTORY]
79
+
80
+ Delete database dump files (`*.dump`).
81
+
82
+ Example: `geordi delete_dumps` or `geordy delete_dumps ~/tmp/dumps`
83
+
84
+ Recursively search for files ending in `*.dump` and offer to delete those. When
85
+ no argument is given, two default directories are searched for dump files: the
86
+ current working directory and `~/dumps` (for dumps created with geordi).
87
+
88
+ Geordi will ask for confirmation before actually deleting files.
89
+
78
90
  ### `geordi deploy [STAGE]`
79
91
 
80
92
  Guided deployment across branches.
@@ -105,6 +117,26 @@ instead of `cap deploy:migrations`. You can force using `deploy` by passing the
105
117
  -M option: `geordi deploy -M staging`.
106
118
 
107
119
 
120
+ ### `geordi drop_databases`
121
+
122
+ Delete local MySQL/MariaDB and Postgres databases that are not whitelisted.
123
+
124
+ Example: `geordi drop_databases`
125
+
126
+ Check both MySQL/MariaDB and Postgres on the machine running geordi for databases
127
+ and offer to delete them. Excluded are databases that are whitelisted. This comes
128
+ in handy when you're keeping your currently active projects in the whitelist files
129
+ and perform regular housekeeping with Geordi.
130
+
131
+ When called with `-P` or `-M` options, only handles Postgres resp. MySQL/MariaDB.
132
+
133
+ When called with `--postgres <port or local socket>` or `--mysql <port or local socket>`,
134
+ will instruct the underlying management commands to use those connection methods
135
+ instead of the defaults. This is useful when running multiple installations.
136
+
137
+ Geordi will ask for confirmation before actually dropping databases and will
138
+ offer to edit the whitelist instead.
139
+
108
140
  ### `geordi dump [TARGET]`
109
141
 
110
142
  Handle dumps (see `geordi help dump` for details).
@@ -97,7 +97,7 @@ Feature: The cucumber command
97
97
 
98
98
  When I run `geordi cucumber --verbose features`
99
99
  Then the output should contain "# Running features"
100
- And the output should match /^> .*cucumber .*--tags ~@solo/
100
+ And the output should match /^> .*cucumber .*--tags \"~@solo\"/
101
101
  And the output should contain "# Running @solo features"
102
102
  And the output should match /^> .*cucumber .*--tags @solo/
103
103
 
@@ -122,7 +122,7 @@ Feature: The cucumber command
122
122
 
123
123
  When I run `geordi cucumber features/no_solo --verbose`
124
124
  Then the output should contain "# Running features"
125
- And the output should match /^> .*cucumber .*--tags ~@solo/
125
+ And the output should match /^> .*cucumber .*--tags \"~@solo\"/
126
126
  But the output should not contain "# Running @solo features"
127
127
 
128
128
 
@@ -0,0 +1,60 @@
1
+ desc 'drop-databases', 'Delete local non-whitelisted databases'
2
+ long_desc <<-LONGDESC
3
+
4
+ Delete local MySQL/MariaDB and Postgres databases that are not whitelisted.
5
+
6
+ Example: `geordi drop_databases`
7
+
8
+ Check both MySQL/MariaDB and Postgres on the machine running geordi for databases
9
+ and offer to delete them. Excluded are databases that are whitelisted. This comes
10
+ in handy when you're keeping your currently active projects in the whitelist files
11
+ and perform regular housekeeping with Geordi.
12
+
13
+ When called with `-P` or `-M` options, only handles Postgres resp. MySQL/MariaDB.
14
+
15
+ When called with `--postgres <port or local socket>` or `--mysql <port or local socket>`,
16
+ will instruct the underlying management commands to use those connection methods
17
+ instead of the defaults. This is useful when running multiple installations.
18
+ LONGDESC
19
+
20
+ option :postgres_only, :aliases => '-P', :type => :boolean,
21
+ :desc => 'Only clean Postgres', :default => false
22
+ option :mysql_only, :aliases => '-M', :type => :boolean,
23
+ :desc => 'Only clean MySQL/MariaDB', :default => false
24
+ option :postgres, :banner => 'PORT_OR_SOCKET',
25
+ :desc => 'Use Postgres port or socket'
26
+ option :mysql, :banner => 'PORT_OR_SOCKET',
27
+ :desc => 'Use MySQL/MariaDB port or socket'
28
+
29
+ def drop_databases
30
+ require 'geordi/db_cleaner'
31
+ fail '-P and -M are mutually exclusive' if options.postgres_only and options.mysql_only
32
+ mysql_flags = nil
33
+ postgres_flags = nil
34
+
35
+ unless options.mysql.nil?
36
+ begin
37
+ mysql_port = Integer(options.mysql)
38
+ mysql_flags = "--port=#{mysql_port} --protocol=TCP"
39
+ rescue AttributeError
40
+ unless File.exist? options.mysql
41
+ fail "Path #{options.mysql} is not a valid MySQL socket"
42
+ end
43
+ mysql_flags = "--socket=#{options.mysql}"
44
+ end
45
+ end
46
+
47
+ unless options.postgres.nil?
48
+ postgres_flags = "--port=#{options.postgres}"
49
+ end
50
+
51
+ extra_flags = {'mysql' => mysql_flags,
52
+ 'postgres' => postgres_flags
53
+ }
54
+ cleaner = DBCleaner.new(extra_flags)
55
+ cleaner.clean_mysql unless options.postgres_only
56
+ cleaner.clean_postgres unless options.mysql_only
57
+
58
+ success 'Done.'
59
+ end
60
+
@@ -0,0 +1,46 @@
1
+ desc 'delete_dumps [DIRECTORY]', 'delete database dump files (*.dump)'
2
+ long_desc <<-LONGDESC
3
+ Delete database dump files (`*.dump`).
4
+
5
+ Example: `geordi delete_dumps` or `geordy delete_dumps ~/tmp/dumps`
6
+
7
+ Recursively search for files ending in `*.dump` and offer to delete those. When
8
+ no argument is given, two default directories are searched for dump files: the
9
+ current working directory and `~/dumps` (for dumps created with geordi).
10
+
11
+ Geordi will ask for confirmation before actually deleting files.
12
+
13
+ LONGDESC
14
+
15
+ def delete_dumps(dump_directory = nil)
16
+ deletable_dumps = []
17
+ if dump_directory.nil?
18
+ dump_directories = [
19
+ File.join(Dir.home, 'dumps'),
20
+ Dir.pwd
21
+ ]
22
+ else
23
+ dump_directories = [dump_directory]
24
+ end
25
+ announce 'Looking for *.dump in ' << dump_directories.join(',')
26
+ dump_directories.each do |d|
27
+ d2 = File.expand_path(d)
28
+ unless File.directory? File.realdirpath(d2)
29
+ warn "Directory #{d2} does not exist"
30
+ next
31
+ end
32
+ deletable_dumps.concat(Dir.glob("#{d2}/**/*.dump"))
33
+ end
34
+ if deletable_dumps.empty?
35
+ success 'No dumps to delete' if deletable_dumps.empty?
36
+ exit 0
37
+ end
38
+ deletable_dumps.uniq!
39
+ note 'The following dumps can be deleted:'
40
+ puts
41
+ puts deletable_dumps
42
+ prompt 'Delete those dumps', 'n', /y|yes/ or fail 'Cancelled.'
43
+ deletable_dumps.each do |dump|
44
+ File.delete dump unless File.directory? dump
45
+ end
46
+ end
@@ -0,0 +1,239 @@
1
+ require 'fileutils'
2
+ require 'open3'
3
+ require 'tempfile'
4
+
5
+ module Geordi
6
+ class DBCleaner
7
+ include Geordi::Interaction
8
+
9
+ def initialize(extra_flags)
10
+ puts 'Please enter your sudo password if asked, for db operations as system users'
11
+ puts "We're going to run `sudo -u postgres psql` for PostgreSQL"
12
+ puts ' and `sudo mysql` for MariaDB (which uses PAM auth)'
13
+ `sudo true`
14
+ fail 'sudo access is required for database operations as database users' if $? != 0
15
+ @derivative_dbname = /_(test\d?|development|cucumber)$/
16
+ base_directory = ENV['XDG_CONFIG_HOME']
17
+ base_directory = "#{Dir.home}" if base_directory.nil?
18
+ @whitelist_directory = File.join(base_directory, '.config', 'geordi', 'whitelists')
19
+ FileUtils.mkdir_p(@whitelist_directory) unless File.directory? @whitelist_directory
20
+ @mysql_command = decide_mysql_command(extra_flags['mysql'])
21
+ @postgres_command = decide_postgres_command(extra_flags['postgres'])
22
+ end
23
+
24
+ def edit_whitelist(dbtype)
25
+ whitelist = whitelist_fname(dbtype)
26
+ if File.exist? whitelist
27
+ whitelisted_dbs = Geordi::Util.stripped_lines(File.read(whitelist))\
28
+ .delete_if { |l| l.start_with? '#' }
29
+ else
30
+ whitelisted_dbs = Array.new
31
+ end
32
+ all_dbs = list_all_dbs(dbtype)
33
+ tmp = Tempfile.open("geordi_whitelist_#{dbtype}")
34
+ tmp.write <<-HEREDOC
35
+ # Put each whitelisted database on a new line.
36
+ # System databases will never be deleted.
37
+ # When you whitelist foo, foo_development and foo_test\\d? are whitelisted, too.
38
+ # This works even if foo does not exist. Also, you will only see foo in this list.
39
+ #
40
+ # Syntax: keep foo
41
+ # drop bar
42
+ HEREDOC
43
+ tmpfile_content = Array.new
44
+ all_dbs.each do |db|
45
+ next if is_whitelisted?(dbtype, db)
46
+ next if is_protected?(dbtype, db)
47
+ db.sub!(@derivative_dbname, '')
48
+ tmpfile_content.push(['drop', db])
49
+ end
50
+ whitelisted_dbs.each do |db|
51
+ tmpfile_content.push(['keep', db])
52
+ end
53
+ tmpfile_content.sort_by! { |k| k[1] }
54
+ tmpfile_content.uniq!
55
+ tmpfile_content.each do |line|
56
+ tmp.write("#{line[0]} #{line[1]}\n")
57
+ end
58
+ tmp.close
59
+ texteditor = Geordi::Util.decide_texteditor
60
+ system("#{texteditor} #{tmp.path}")
61
+ File.open(tmp.path, 'r') do |wl_edited|
62
+ whitelisted_dbs = Array.new
63
+ whitelist_storage = File.open(whitelist, 'w')
64
+ lines = Geordi::Util.stripped_lines(wl_edited.read)
65
+ lines.each do |line|
66
+ next if line.start_with?('#')
67
+ fail 'Invalid edit to whitelist file' unless line.split.length == 2
68
+ fail 'Invalid edit to whitelist file' unless %w[keep drop k d].include? line.split[0]
69
+ db_status, db_name = line.split
70
+ if db_status == 'keep'
71
+ whitelisted_dbs.push db_name
72
+ whitelist_storage.write(db_name << "\n")
73
+ end
74
+ end
75
+ whitelist_storage.close
76
+ end
77
+ end
78
+
79
+ def decide_mysql_command(extra_flags)
80
+ cmd = 'sudo mysql'
81
+ unless extra_flags.nil?
82
+ if extra_flags.include? 'port'
83
+ port = Integer(extra_flags.split('=')[1].split()[0])
84
+ fail "Port #{port} is not open" unless Geordi::Util.is_port_open? port
85
+ end
86
+ cmd << " #{extra_flags}"
87
+ end
88
+ Open3.popen3("#{cmd} -e 'QUIT'") do |stdin, stdout, stderr, thread|
89
+ break if thread.value.exitstatus == 0
90
+ # sudo mysql was not successful, switching to mysql-internal user management
91
+ mysql_error = stderr.read.lines[0].chomp.strip.split[1]
92
+ if %w[1045 1698].include? mysql_error # authentication failed
93
+ cmd = 'mysql -uroot'
94
+ cmd << " #{extra_flags}" unless extra_flags.nil?
95
+ unless File.exist? File.join(Dir.home, '.my.cnf')
96
+ puts "Please enter your MySQL/MariaDB password for account 'root'."
97
+ warn "You should create a ~/.my.cnf file instead, or you'll need to enter your MySQL root password for each db."
98
+ warn "See https://makandracards.com/makandra/50813-store-mysql-passwords-for-development for more information."
99
+ cmd << ' -p' # need to ask for password now
100
+ end
101
+ Open3.popen3("#{cmd} -e 'QUIT'") do |stdin2, stdout2, stderr2, thread2|
102
+ fail 'Could not connect to MySQL/MariaDB' unless thread2.value.exitstatus == 0
103
+ end
104
+ elsif mysql_error == '2013' # connection to port or socket failed
105
+ fail 'MySQL/MariaDB connection failed, is this the correct port?'
106
+ end
107
+ end
108
+ return cmd
109
+ end
110
+ private :decide_mysql_command
111
+
112
+ def decide_postgres_command(extra_flags)
113
+ cmd = 'sudo -u postgres psql'
114
+ unless extra_flags.nil?
115
+ begin
116
+ port = Integer(extra_flags.split('=')[1])
117
+ fail "Port #{port} is not open" unless Geordi::Util.is_port_open? port
118
+ rescue ArgumentError
119
+ socket = extra_flags.split('=')[1]
120
+ fail "Socket #{socket} does not exist" unless File.exist? socket
121
+ end
122
+ cmd << " #{extra_flags}"
123
+ end
124
+ return cmd
125
+ end
126
+ private :decide_postgres_command
127
+
128
+ def list_all_dbs(dbtype)
129
+ if dbtype == 'postgres'
130
+ return list_all_postgres_dbs
131
+ else
132
+ return list_all_mysql_dbs
133
+ end
134
+ end
135
+
136
+ def list_all_postgres_dbs
137
+ `#{@postgres_command} -t -A -c 'SELECT DATNAME FROM pg_database WHERE datistemplate = false'`.split
138
+ end
139
+
140
+ def list_all_mysql_dbs
141
+ if @mysql_command.include? '-p'
142
+ puts "Please enter your MySQL/MariaDB account 'root' for: list all databases"
143
+ end
144
+ `#{@mysql_command} -B -N -e 'show databases'`.split
145
+ end
146
+
147
+ def clean_mysql
148
+ announce 'Checking for MySQL databases'
149
+ database_list = list_all_dbs('mysql')
150
+ # confirm_deletion includes option for whitelist editing
151
+ deletable_dbs = confirm_deletion('mysql', database_list)
152
+ return if deletable_dbs.nil?
153
+ deletable_dbs.each do |db|
154
+ if @mysql_command.include? '-p'
155
+ puts "Please enter your MySQL/MariaDB account 'root' for: DROP DATABASE #{db}"
156
+ else
157
+ note "Dropping MySQL/MariaDB database #{db}"
158
+ end
159
+ `#{@mysql_command} -e 'DROP DATABASE \`#{db}\`;'`
160
+ end
161
+ end
162
+
163
+ def clean_postgres
164
+ announce 'Checking for Postgres databases'
165
+ database_list = list_all_dbs('postgres')
166
+ deletable_dbs = confirm_deletion('postgres', database_list)
167
+ return if deletable_dbs.nil?
168
+ deletable_dbs.each do |db|
169
+ note "Dropping PostgreSQL database `#{db}`."
170
+ `#{@postgres_command} -c 'DROP DATABASE "#{db}";'`
171
+ end
172
+ end
173
+
174
+ def whitelist_fname(dbtype)
175
+ File.join(@whitelist_directory, dbtype) << '.txt'
176
+ end
177
+
178
+ def confirm_deletion(dbtype, database_list)
179
+ proceed = ''
180
+ until %w[y n].include? proceed
181
+ deletable_dbs = filter_whitelisted(dbtype, database_list)
182
+ if deletable_dbs.empty?
183
+ note "No #{dbtype} databases found that were not whitelisted"
184
+ if prompt('Edit the whitelist? [y]es or [n]o') == 'y'
185
+ proceed = 'e'
186
+ else
187
+ return []
188
+ end
189
+ end
190
+ if proceed.empty?
191
+ note "The following #{dbtype} databases are not whitelisted and could be deleted:"
192
+ deletable_dbs.sort.each do |db|
193
+ puts db
194
+ end
195
+ note "Those #{dbtype} databases are not whitelisted and could be deleted."
196
+ proceed = prompt('Proceed? [y]es, [n]o or [e]dit whitelist')
197
+ end
198
+ case proceed
199
+ when 'e'
200
+ proceed = '' # reset user selection
201
+ edit_whitelist dbtype
202
+ when 'n'
203
+ success 'Not deleting anything'
204
+ return []
205
+ when 'y'
206
+ return deletable_dbs
207
+ end
208
+ end
209
+ end
210
+ private :confirm_deletion
211
+
212
+ def is_protected?(dbtype, database_name)
213
+ protected = {
214
+ 'mysql' => %w[mysql information_schema performance_schema sys],
215
+ 'postgres' => ['postgres'],
216
+ }
217
+ protected[dbtype].include? database_name
218
+ end
219
+
220
+ def is_whitelisted?(dbtype, database_name)
221
+ if File.exist? whitelist_fname(dbtype)
222
+ whitelist_content = Geordi::Util.stripped_lines(File.open(whitelist_fname(dbtype), 'r').read)
223
+ else
224
+ whitelist_content = Array.new
225
+ end
226
+ whitelist_content.include? database_name.sub(@derivative_dbname, '')
227
+ end
228
+
229
+ def filter_whitelisted(dbtype, database_list)
230
+ # n.b. `delete` means 'delete from list of dbs that should be deleted in this context
231
+ # i.e. `delete` means 'keep this database'
232
+ deletable_dbs = database_list.dup
233
+ deletable_dbs.delete_if { |db| is_whitelisted?(dbtype, db) if File.exist? whitelist_fname(dbtype) }
234
+ deletable_dbs.delete_if { |db| is_protected?(dbtype, db) }
235
+ deletable_dbs.delete_if { |db| db.start_with? '#' }
236
+ end
237
+ private :filter_whitelisted
238
+ end
239
+ end
@@ -1,31 +1,42 @@
1
+ # Use the methods in this file to communicate with the user
2
+ #
1
3
  module Geordi
2
4
  module Interaction
3
5
 
6
+ # Start your command by `announce`-ing what you're about to do
4
7
  def announce(text)
5
8
  message = "\n# #{text}"
6
9
  puts "\e[4;34m#{message}\e[0m" # blue underline
7
10
  end
8
11
 
12
+ # Any hints, comments, infos or explanations should be `note`d. Please do
13
+ # not print any output (data, file contents, lists) with `note`.
9
14
  def note(text)
10
15
  puts '> ' + text
11
16
  end
12
17
 
18
+ # Like `note`, but yellow. Use to warn the user.
13
19
  def warn(text)
14
20
  message = "> #{text}"
15
21
  puts "\e[33m#{message}\e[0m" # yellow
16
22
  end
17
23
 
24
+ # Like `note`, but pink. Use to print (bash) commands.
25
+ # Also see Util.system!
18
26
  def note_cmd(text)
19
27
  message = "> #{text}"
20
28
  puts "\e[35m#{message}\e[0m" # pink
21
29
  end
22
30
 
31
+ # Exit execution with status code 1 and give a short note what happened,
32
+ # e.g. "Failed" or "Cancelled"
23
33
  def fail(text)
24
34
  message = "\nx #{text}"
25
35
  puts "\e[31m#{message}\e[0m" # red
26
36
  exit(1)
27
37
  end
28
38
 
39
+ # When you're done, inform the user with a `success` and a short message
29
40
  def success(text)
30
41
  message = "\n> #{text}"
31
42
  puts "\e[32m#{message}\e[0m" # green
data/lib/geordi/util.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'geordi/interaction'
2
+ require 'socket'
2
3
 
3
4
  module Geordi
4
5
  class Util
@@ -79,6 +80,37 @@ module Geordi
79
80
  end
80
81
  end
81
82
 
83
+ # try to guess user's favorite cli text editor
84
+ def decide_texteditor
85
+ %w[$VISUAL $EDITOR /usr/bin/editor vi].each do |texteditor|
86
+ if cmd_exists? texteditor and texteditor.start_with? '$'
87
+ return ENV[texteditor[1..-1]]
88
+ elsif cmd_exists? texteditor
89
+ return texteditor
90
+ end
91
+ end
92
+ end
93
+
94
+ # check if given cmd is executable. Absolute path or command in $PATH allowed.
95
+ def cmd_exists? cmd
96
+ system("which #{cmd} > /dev/null")
97
+ return $?.exitstatus.zero?
98
+ end
99
+
100
+ def is_port_open?(port)
101
+ begin
102
+ socket = TCPSocket.new('127.0.0.1', port)
103
+ socket.close
104
+ return true
105
+ rescue Errno::ECONNREFUSED
106
+ return false
107
+ end
108
+ end
109
+
110
+ # splint lines e.g. read from a file into lines and clean those up
111
+ def stripped_lines(input_string)
112
+ input_string.lines.map(&:chomp).map(&:strip)
113
+ end
82
114
  end
83
115
  end
84
116
  end
@@ -1,3 +1,3 @@
1
1
  module Geordi
2
- VERSION = '1.8.0'
2
+ VERSION = '1.9.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geordi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henning Koch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-28 00:00:00.000000000 Z
11
+ date: 2018-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -97,6 +97,8 @@ files:
97
97
  - lib/geordi/commands/create_database_yml.rb
98
98
  - lib/geordi/commands/create_databases.rb
99
99
  - lib/geordi/commands/cucumber.rb
100
+ - lib/geordi/commands/db_clean.rb
101
+ - lib/geordi/commands/delete_dumps.rb
100
102
  - lib/geordi/commands/deploy.rb
101
103
  - lib/geordi/commands/dump.rb
102
104
  - lib/geordi/commands/eurest.rb
@@ -117,6 +119,7 @@ files:
117
119
  - lib/geordi/commands/vnc.rb
118
120
  - lib/geordi/commands/with_rake.rb
119
121
  - lib/geordi/cucumber.rb
122
+ - lib/geordi/db_cleaner.rb
120
123
  - lib/geordi/dump_loader.rb
121
124
  - lib/geordi/firefox_for_selenium.rb
122
125
  - lib/geordi/gitpt.rb