cartage 1.0

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.
@@ -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'