cartage 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +8 -0
- data/.gemtest +1 -0
- data/.minitest.rb +2 -0
- data/.travis.yml +36 -0
- data/Cartage.yml.rdoc +271 -0
- data/Contributing.rdoc +66 -0
- data/Gemfile +9 -0
- data/History.rdoc +5 -0
- data/Licence.rdoc +27 -0
- data/Manifest.txt +24 -0
- data/README.rdoc +192 -0
- data/Rakefile +62 -0
- data/bin/cartage +8 -0
- data/cartage.yml.sample +172 -0
- data/lib/cartage.rb +496 -0
- data/lib/cartage/command.rb +59 -0
- data/lib/cartage/config.rb +193 -0
- data/lib/cartage/manifest.rb +218 -0
- data/lib/cartage/manifest/commands.rb +106 -0
- data/lib/cartage/pack_command.rb +14 -0
- data/lib/cartage/plugin.rb +80 -0
- data/test/minitest_config.rb +72 -0
- data/test/test_cartage.rb +261 -0
- data/test/test_cartage_config.rb +47 -0
- metadata +348 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'cartage'
|
2
|
+
require 'cmdparse'
|
3
|
+
|
4
|
+
class Cartage
|
5
|
+
# The base Cartage command object. This is used to provide commands with a
|
6
|
+
# reference to the Cartage instance (<tt>@cartage</tt>) that is running under
|
7
|
+
# the main command-line program.
|
8
|
+
#
|
9
|
+
# class Cartage::InfoCommand < Cartage::Command
|
10
|
+
# def initialize(cartage)
|
11
|
+
# super(cartage, 'info')
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# def perform
|
15
|
+
# puts 'info'
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Cartage::Command changes the class-based command protocol from CmdParse so
|
20
|
+
# that you must define #perform instead of #execute. This is so that certain
|
21
|
+
# common behaviours (such as common config resolution) can be performed for
|
22
|
+
# all commands.
|
23
|
+
class Command < ::CmdParse::Command
|
24
|
+
# Create a ::CmdParse::Command with the given name. Save a reference to the
|
25
|
+
# provided cartage object.
|
26
|
+
def initialize(cartage, name, takes_commands: false)
|
27
|
+
if self.instance_of?(Cartage::Command)
|
28
|
+
raise ArgumentError, 'Cartage::Command cannot be instantiated.'
|
29
|
+
end
|
30
|
+
|
31
|
+
super(name, takes_commands: takes_commands)
|
32
|
+
|
33
|
+
@cartage = cartage
|
34
|
+
end
|
35
|
+
|
36
|
+
# Run the command. Children must *not* implement this method.
|
37
|
+
def execute(*args)
|
38
|
+
@cartage.send(:resolve_config!, *Array(with_plugins))
|
39
|
+
perform(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# :method: perform
|
44
|
+
#
|
45
|
+
# Perform the action of the command. Children *must* implement this method.
|
46
|
+
|
47
|
+
# This optional method is implemented by a plug-in to instruct Cartage to
|
48
|
+
# resolve the plug-in configuration for the specified plug-ins. Specify the
|
49
|
+
# plug-ins to be resolved as an array of Symbols.
|
50
|
+
#
|
51
|
+
# Plug-ins resolve by overriding the private method
|
52
|
+
# Cartage::Plugin#resolve_config!.
|
53
|
+
def with_plugins
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
require_relative 'pack_command'
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
class Cartage
|
5
|
+
# The Cartage configuration structure. The supported Cartage-wide
|
6
|
+
# configuration fields are:
|
7
|
+
#
|
8
|
+
# +target+:: The target where the final Cartage package will be created. Sets
|
9
|
+
# Cartage#target. Equivalent to <tt>--target</tt>.
|
10
|
+
# +name+:: The name of the package to create. Sets Cartage#name. Equivalent
|
11
|
+
# to <tt>--name</tt>.
|
12
|
+
# +root_path+:: The root path of the application. Sets Cartage#root_path.
|
13
|
+
# Equivalent to <tt>--root-path</tt>.
|
14
|
+
# +timestamp+:: The timestamp for the final package. Sets Cartage#timestamp.
|
15
|
+
# Equivalent to <tt>--timestamp</tt>.
|
16
|
+
# +bundle_cache+:: The bundle cache. Sets Cartage#bundle_cache. Equivalent to
|
17
|
+
# <tt>--bundle-cache</tt>.
|
18
|
+
# +without+:: Groups to exclude from bundle installation. Sets
|
19
|
+
# Cartage#without_groups. Equivalent to <tt>--without</tt>.
|
20
|
+
# This value should be provided as an array.
|
21
|
+
# +plugins+:: A dictionary for plug-in configuration groups. See below for
|
22
|
+
# more information.
|
23
|
+
#
|
24
|
+
# Cartage configuration is not typically partitioned by an environment label,
|
25
|
+
# but can be. See the examples below for details.
|
26
|
+
#
|
27
|
+
# == Plug-Ins
|
28
|
+
#
|
29
|
+
# Plug-ins also keep configuration in the Cartage configuration structure,
|
30
|
+
# but as dictionary (hash) structures under the +plugins+ field. Each plug-in
|
31
|
+
# has its own key based on its name, so that the Manifest plug-in (if it had
|
32
|
+
# storable configuration values) would keep its configuration in a +manifest+
|
33
|
+
# key. See the examples below for details.
|
34
|
+
#
|
35
|
+
# == Loading Configuration
|
36
|
+
#
|
37
|
+
# When <tt>--config-file</tt> is specified, the configuration file will be
|
38
|
+
# loaded and parsed. If a filename is given, that file will be loaded. If a
|
39
|
+
# filename is not given, Cartage will look for the configuration in the
|
40
|
+
# following locations:
|
41
|
+
#
|
42
|
+
# * config/cartage.yml
|
43
|
+
# * ./cartage.yml
|
44
|
+
# * $HOME/.config/cartage.yml
|
45
|
+
# * $HOME/.cartage.yml
|
46
|
+
# * /etc/cartage.yml
|
47
|
+
#
|
48
|
+
# The contents of the configuration file are evaluated through ERB and then
|
49
|
+
# parsed from YAML and converted to nested OpenStruct objects. The basic
|
50
|
+
# environment example below would look like:
|
51
|
+
#
|
52
|
+
# #<OpenStruct development=
|
53
|
+
# #<OpenStruct without=["test", "development", "assets"]>
|
54
|
+
# >
|
55
|
+
#
|
56
|
+
# == Examples
|
57
|
+
#
|
58
|
+
# Basic Cartage configuration:
|
59
|
+
#
|
60
|
+
# ---
|
61
|
+
# without:
|
62
|
+
# - test
|
63
|
+
# - development
|
64
|
+
# - assets
|
65
|
+
#
|
66
|
+
# With an environment set:
|
67
|
+
#
|
68
|
+
# ---
|
69
|
+
# development:
|
70
|
+
# without:
|
71
|
+
# - test
|
72
|
+
# - development
|
73
|
+
# - assets
|
74
|
+
#
|
75
|
+
# With the Manifest plug-in (note: the Manifest plug-in does *not* have
|
76
|
+
# configurable options; this is for example purposes only).
|
77
|
+
#
|
78
|
+
# ---
|
79
|
+
# without:
|
80
|
+
# - test
|
81
|
+
# - development
|
82
|
+
# - assets
|
83
|
+
# manifest:
|
84
|
+
# format: json
|
85
|
+
#
|
86
|
+
# With the Manifest plug-in and an environment:
|
87
|
+
#
|
88
|
+
# ---
|
89
|
+
# development:
|
90
|
+
# without:
|
91
|
+
# - test
|
92
|
+
# - development
|
93
|
+
# - assets
|
94
|
+
# manifest:
|
95
|
+
# format: json
|
96
|
+
class Config < OpenStruct
|
97
|
+
#:stopdoc:
|
98
|
+
DEFAULT_CONFIG_FILES = %w(
|
99
|
+
config/cartage.yml
|
100
|
+
./cartage.yml
|
101
|
+
~/.config/cartage.yml
|
102
|
+
~/.cartage.yml
|
103
|
+
/etc/cartage.yml
|
104
|
+
)
|
105
|
+
#:startdoc:
|
106
|
+
|
107
|
+
class << self
|
108
|
+
# Load a Cartage configuration file.
|
109
|
+
def load(filename)
|
110
|
+
config_file = resolve_config_file(filename)
|
111
|
+
config = YAML.load(ERB.new(config_file.read, nil, '%<>-').result)
|
112
|
+
new(ostructify(config))
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def resolve_config_file(filename)
|
118
|
+
return unless filename
|
119
|
+
|
120
|
+
files = if filename == :default
|
121
|
+
DEFAULT_CONFIG_FILES
|
122
|
+
else
|
123
|
+
[ filename ]
|
124
|
+
end
|
125
|
+
|
126
|
+
file = files.find { |f| Pathname(f).expand_path.exist? }
|
127
|
+
|
128
|
+
if file
|
129
|
+
Pathname(file).expand_path
|
130
|
+
else
|
131
|
+
message = if filename
|
132
|
+
"Configuration file #{filename} does not exist."
|
133
|
+
else
|
134
|
+
"No default configuration file found."
|
135
|
+
end
|
136
|
+
|
137
|
+
raise ArgumentError, message
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def ostructify(hash)
|
142
|
+
hash = hash.dup
|
143
|
+
hash.keys.each do |k|
|
144
|
+
hash[k.to_sym] = ostructify_recursively(hash.delete(k))
|
145
|
+
end
|
146
|
+
OpenStruct.new(hash)
|
147
|
+
end
|
148
|
+
|
149
|
+
def ostructify_recursively(object)
|
150
|
+
case object
|
151
|
+
when ::Array
|
152
|
+
object.map! { |i| ostructify_recursively(i) }
|
153
|
+
when ::OpenStruct
|
154
|
+
object = ostructify(object.to_h)
|
155
|
+
when ::Hash
|
156
|
+
object = ostructify(object)
|
157
|
+
end
|
158
|
+
|
159
|
+
object
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Convert the entire Config structure to a hash recursively.
|
164
|
+
def to_h
|
165
|
+
hashify(self)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Override the default #to_yaml implementation.
|
169
|
+
def to_yaml
|
170
|
+
to_h.to_yaml
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
def hashify(ostruct)
|
175
|
+
{}.tap { |hash|
|
176
|
+
ostruct.each_pair do |k, v|
|
177
|
+
hash[k.to_s] = hashify_recursively(v)
|
178
|
+
end
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
def hashify_recursively(object)
|
183
|
+
case object
|
184
|
+
when ::Array
|
185
|
+
object.map! { |i| hashify_recursively(i) }
|
186
|
+
when ::OpenStruct, ::Hash
|
187
|
+
object = hashify(object)
|
188
|
+
end
|
189
|
+
|
190
|
+
object
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
# Manage and use the package manifest ('Manifest.txt') and the ignore file
|
3
|
+
# ('.cartignore').
|
4
|
+
class Cartage::Manifest < Cartage::Plugin
|
5
|
+
# This exception is raised if the package manifest is missing.
|
6
|
+
MissingError = Class.new(StandardError) do
|
7
|
+
def message
|
8
|
+
<<-exception
|
9
|
+
Cartage cannot create a package without a Manifest.txt file. You may generate
|
10
|
+
or update the Manifest.txt file with the following command:
|
11
|
+
|
12
|
+
exception
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
DIFF = if system('gdiff', __FILE__, __FILE__) #:nodoc:
|
17
|
+
'gdiff'
|
18
|
+
else
|
19
|
+
'diff'
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(cartage) #:nodoc:
|
23
|
+
@cartage = cartage
|
24
|
+
end
|
25
|
+
|
26
|
+
# Resolve the Manifest to something that can be used by <tt>tar -T</tt>. The
|
27
|
+
# manifest should be relative to the files in the repository, as reported
|
28
|
+
# from <tt>git ls-files</tt>. +tar+ requires either full paths, or files
|
29
|
+
# relative to the directory you are packaging from (and we want relative
|
30
|
+
# files).
|
31
|
+
#
|
32
|
+
# This reads the manifest, prunes it with package ignores, and then writes it
|
33
|
+
# in a GNU tar compatible format as a tempfile. It does this by taking the
|
34
|
+
# expanded version of +path+, grabbing the basename, and inserting that in
|
35
|
+
# front of every line in the Manifest.
|
36
|
+
#
|
37
|
+
# If +path+ is not provided, Dir.pwd is used.
|
38
|
+
def resolve(path = nil, command: nil) # :yields: resolved manifest filename
|
39
|
+
raise MissingError unless manifest_file.exist?
|
40
|
+
|
41
|
+
data = strip_comments_and_empty_lines(manifest_file.readlines)
|
42
|
+
raise "Manifest.txt is empty." if data.empty?
|
43
|
+
|
44
|
+
path = Pathname(path || Dir.pwd).expand_path.basename
|
45
|
+
tmpfile = Tempfile.new('Manifest')
|
46
|
+
|
47
|
+
tmpfile.puts prune(data, with_slugignore: true).map { |line|
|
48
|
+
path.join(line).to_s
|
49
|
+
}.join("\n")
|
50
|
+
|
51
|
+
tmpfile.close
|
52
|
+
|
53
|
+
yield tmpfile.path
|
54
|
+
ensure
|
55
|
+
if tmpfile
|
56
|
+
tmpfile.close
|
57
|
+
tmpfile.unlink
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generates Manifest.txt.
|
62
|
+
def generate
|
63
|
+
create_file_list(manifest_file)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Checks Manifest.txt
|
67
|
+
def check
|
68
|
+
raise MissingError unless manifest_file.exist?
|
69
|
+
tmp = create_file_list('Manifest.tmp')
|
70
|
+
system(DIFF, '-du', manifest_file.basename.to_s, tmp.to_s)
|
71
|
+
$?.success?
|
72
|
+
ensure
|
73
|
+
tmp.unlink if tmp
|
74
|
+
end
|
75
|
+
|
76
|
+
# Installs the default .cartignore file.
|
77
|
+
def install_default_ignore(mode: nil)
|
78
|
+
mode = mode.to_s.downcase.strip
|
79
|
+
save = mode || !ignore_file.exist?
|
80
|
+
|
81
|
+
if mode == :merge
|
82
|
+
data = strip_comments_and_empty_lines(ignore_file.readlines)
|
83
|
+
|
84
|
+
if data.empty?
|
85
|
+
data = DEFAULT_IGNORE
|
86
|
+
else
|
87
|
+
data += strip_comments_and_empty_lines(DEFAULT_IGNORE.split($/))
|
88
|
+
data = data.uniq.join("\n")
|
89
|
+
end
|
90
|
+
else
|
91
|
+
data = DEFAULT_IGNORE
|
92
|
+
end
|
93
|
+
|
94
|
+
ignore_file.open('w') { |f| f.puts data } if save
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def ignore_file
|
100
|
+
@ignore_file ||= @cartage.root_path.join('.cartignore')
|
101
|
+
end
|
102
|
+
|
103
|
+
def slugignore_file
|
104
|
+
@slugignore_file ||= @cartage.root_path.join('.slugignore')
|
105
|
+
end
|
106
|
+
|
107
|
+
def manifest_file
|
108
|
+
@manifest_file ||= @cartage.root_path.join('Manifest.txt')
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_file_list(filename)
|
112
|
+
Pathname(filename).tap { |file|
|
113
|
+
file.open('w') { |f|
|
114
|
+
f.puts prune(%x(git ls-files).split.map(&:chomp)).sort.join("\n")
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def ignore_patterns(with_slugignore: false)
|
120
|
+
pats = if ignore_file.exist?
|
121
|
+
ignore_file.readlines
|
122
|
+
elsif with_slugignore && slugignore_file.exist?
|
123
|
+
slugignore_file.readlines
|
124
|
+
else
|
125
|
+
DEFAULT_IGNORE.split($/)
|
126
|
+
end
|
127
|
+
|
128
|
+
pats = strip_comments_and_empty_lines(pats)
|
129
|
+
|
130
|
+
pats.map { |pat|
|
131
|
+
if pat =~ %r{/\z}
|
132
|
+
Regexp.new(%r{\A#{pat}})
|
133
|
+
elsif pat =~ %r{\A/[^*?]+\z}
|
134
|
+
Regexp.new(%r{\A#{pat.sub(%r{\A/}, '')}/})
|
135
|
+
else
|
136
|
+
pat
|
137
|
+
end
|
138
|
+
}.compact
|
139
|
+
end
|
140
|
+
|
141
|
+
def strip_comments_and_empty_lines(list)
|
142
|
+
list.map { |item|
|
143
|
+
item = item.chomp.gsub(/(?:^|[^\\])#.*\z/, '').strip
|
144
|
+
if item.empty?
|
145
|
+
nil
|
146
|
+
else
|
147
|
+
item
|
148
|
+
end
|
149
|
+
}.compact
|
150
|
+
end
|
151
|
+
|
152
|
+
def prune(files, with_slugignore: false)
|
153
|
+
exclusions = ignore_patterns(with_slugignore: with_slugignore)
|
154
|
+
files.reject { |file| prune?(file, exclusions) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def prune?(file, exclusions = ignore_patterns())
|
158
|
+
exclusions.any? do |pat|
|
159
|
+
case pat
|
160
|
+
when /[*?]/
|
161
|
+
File.fnmatch?(pat, file, File::FNM_PATHNAME | File::FNM_EXTGLOB |
|
162
|
+
File::FNM_DOTMATCH)
|
163
|
+
when Regexp
|
164
|
+
file =~ pat
|
165
|
+
else
|
166
|
+
file == pat
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
DEFAULT_IGNORE = <<-'EOM' #:nodoc:
|
172
|
+
# Some of these are in .gitignore, but let’s remove these just in case they got
|
173
|
+
# checked in.
|
174
|
+
|
175
|
+
# Exact files to remove. Matches with ==.
|
176
|
+
.DS_Store
|
177
|
+
.autotest
|
178
|
+
.editorconfig
|
179
|
+
.env
|
180
|
+
.git-wtfrc
|
181
|
+
.gitignore
|
182
|
+
.local.vimrc
|
183
|
+
.lvimrc
|
184
|
+
.cartignore
|
185
|
+
.powenv
|
186
|
+
.rake_tasks~
|
187
|
+
.rspec
|
188
|
+
.rubocop.yml
|
189
|
+
.rvmrc
|
190
|
+
.semaphore-cache
|
191
|
+
.workenv
|
192
|
+
Guardfile
|
193
|
+
README.md
|
194
|
+
bin/build
|
195
|
+
bin/notify-project-board
|
196
|
+
bin/osx-bootstrap
|
197
|
+
bin/setup
|
198
|
+
|
199
|
+
# Patterns to remove. These have a *, **, or ? in them. Uses File.fnmatch with
|
200
|
+
# File::FNM_DOTMATCH and File::FNM_EXTGLOB.
|
201
|
+
*.rbc
|
202
|
+
.*.swp
|
203
|
+
**/.DS_Store
|
204
|
+
|
205
|
+
# Directories to remove. These should end with a slash. Matches as the regular
|
206
|
+
# expression %r{\A#{pattern}}.
|
207
|
+
db/seeds/development/
|
208
|
+
db/seeds/test/
|
209
|
+
# db/seeds/dit/
|
210
|
+
# db/seeds/staging/
|
211
|
+
log/
|
212
|
+
test/
|
213
|
+
tmp/
|
214
|
+
vendor/bundle/
|
215
|
+
EOM
|
216
|
+
end
|
217
|
+
|
218
|
+
require_relative 'manifest/commands'
|