prick 0.2.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.
data/file ADDED
File without changes
@@ -0,0 +1,14 @@
1
+
2
+ module Algorithm
3
+ def follow(object, sym = nil, &block)
4
+ sym.nil? == block_given? or raise "Can't use both symbol and block"
5
+ a = []
6
+ while object
7
+ a << object
8
+ object = block_given? ? yield(object) : object.send(sym)
9
+ end
10
+ a
11
+ end
12
+
13
+ module_function :follow
14
+ end
@@ -0,0 +1,8 @@
1
+
2
+ module FileUtils
3
+ def self.touch_p(file)
4
+ dir = File.dirname(file)
5
+ File.exist?(dir) or FileUtils.mkdir_p(dir)
6
+ touch(file)
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ require 'pg'
2
+
3
+ class PG::Result
4
+ def value() self.getvalue(0, 0) end
5
+
6
+ def each_value(&block)
7
+ if block_given?
8
+ self.each_row { |r| yield(r.first) }
9
+ else
10
+ self.each_row.map { |r| r.first }
11
+ end
12
+ end
13
+
14
+ def empty?() ntuples == 0 end
15
+
16
+ def to_a() self.values end
17
+ end
18
+
@@ -0,0 +1,21 @@
1
+ require 'semantic' # https://github.com/jlindsey/semantic
2
+
3
+ require "prick/constants.rb"
4
+ require "prick/exceptions.rb"
5
+
6
+ require "prick/archive.rb"
7
+ require "prick/build.rb"
8
+ require "prick/command.rb"
9
+ require "prick/database.rb"
10
+ require "prick/dsort.rb"
11
+ require "prick/ensure.rb"
12
+ require "prick/git.rb"
13
+ require "prick/migra.rb"
14
+ require "prick/rdbms.rb"
15
+ require "prick/schema.rb"
16
+ require "prick/version.rb"
17
+ require "prick/project.rb"
18
+
19
+ module Prick
20
+ end
21
+
@@ -0,0 +1,124 @@
1
+
2
+ module Prick
3
+ class DumpFile
4
+ attr_reader :project
5
+ attr_reader :name
6
+ attr_reader :file
7
+ attr_reader :path
8
+
9
+ def initialize(project, name)
10
+ @project = project
11
+ @name = name
12
+ @file = "#{project.name}-#{name}.#{DUMP_EXT}"
13
+ @path = File.join(CACHE_DIR, @file)
14
+ end
15
+
16
+ def exist?() File.exist?(path) end
17
+
18
+ def delete() FileUtils.rm_f(path) end
19
+ end
20
+ end
21
+
22
+ __END__
23
+
24
+
25
+
26
+ class Release
27
+ attr_reader :project
28
+ attr_reader :version
29
+ attr_reader :semver
30
+
31
+ # Associated Database object
32
+ attr_reader :database
33
+
34
+ # Name of release (eg. 'project-1.2.3')
35
+ attr_reader :name
36
+
37
+ # Release file (eb. 'project-1.2.3.dump.gz')
38
+ attr_reader :file
39
+
40
+ # Path to release file
41
+ def path() File.join(RELEASE_DIR, file) end
42
+
43
+ def initialize(project, version_or_semver, database: nil)
44
+ @project = project
45
+ @version, @semver = Semver::parse(version_or_semver)
46
+ @name = "#{@project.name}-#{@semver}"
47
+ @file = @name + RELEASE_DOT_EXT
48
+ @database = database || Database.new(@name, @project.user)
49
+ end
50
+
51
+ def self.glob(project)
52
+ "#{project.name}-*.#{RELEASE_EXT}"
53
+ end
54
+
55
+ # $1 matches the version
56
+ def self.re(project)
57
+ /^#{project.name}-(#{VERSION_RE.source})$/
58
+ end
59
+
60
+ # True if the release file exists
61
+ def cached?() File.exist?(path) end
62
+
63
+ def build() project.build(version) end
64
+
65
+ def unbuild() FileUtils.rm_f(path) end
66
+
67
+ # True if the database exists and is loaded
68
+ def loaded?() database.loaded? end
69
+
70
+ # Create database and load release
71
+ def load(file = nil)
72
+ file ||= path
73
+ !loaded? or raise Error, "Release #{name} is already loaded"
74
+ database.create
75
+ database.load(file)
76
+ end
77
+
78
+ # Reload release
79
+ def reload(file = path) unload; load(file) end
80
+
81
+ # Delete a database. It is not an error if the database doesn't exists
82
+ def unload() database.drop if loaded? end
83
+
84
+ # Remove the release file
85
+ def remove() FileUtils::rm_f(path) end
86
+
87
+ # Compare two release by semantic version
88
+ def <=>(other) self.semver <=> other.semver end
89
+
90
+ # Render self a the name of the release (eg. 'project-1.2.3')
91
+ def to_s() name end
92
+
93
+ # Returns self so that you can do 'release = lookup(version).ensure(:loaded)'
94
+ def ensure(state, expect: true)
95
+ value = self.send(:"#{state}?")
96
+ if value != expect
97
+ state_methods = STATES[state] or raise Error, "Can't change state to #{state.inspect}"
98
+ index = expect ? 0 : 1
99
+ self.send(state_method[index])
100
+ end
101
+ self
102
+ end
103
+ end
104
+
105
+ STATES = {
106
+ cached: [:build, :unbuild],
107
+ loaded: [:load, :unload]
108
+ }
109
+ end
110
+
111
+
112
+
113
+
114
+
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
@@ -0,0 +1,376 @@
1
+
2
+ require "prick/ensure.rb"
3
+ require "prick/dsort.rb"
4
+ require "ext/fileutils.rb" # for ::touch_p
5
+
6
+ module Prick
7
+ class Build
8
+ include Ensure
9
+
10
+ # The associated project object
11
+ attr_reader :project
12
+
13
+ # Version
14
+ attr_reader :version
15
+
16
+ # Build name. Same as `version.to_s`
17
+ def name() version.to_s end
18
+
19
+ # Associated database object
20
+ attr_reader :database
21
+
22
+ # Schema object. Only defined when the build has been checked out
23
+ attr_reader :schema
24
+
25
+ # Migration object. Running the migration on a base release database will
26
+ # mutate it into the current release
27
+ attr_reader :migration
28
+
29
+ # Path to a filesystem node that represents the build on disk. Used to
30
+ # detect if a build is present in the current release's tree. It is
31
+ # possible to infer the base release from the node - either by a naming
32
+ # convention or by reading the file. Build::deref_node_file does that
33
+ def node() raise AbstractMethod end
34
+
35
+ # Base release. Returns nil if version is 0.0.0. Raises an exception if
36
+ # branch is not represented on disk and it can't be inferred from the
37
+ # version (only features)
38
+ def base_release()
39
+ return nil if version.zero?
40
+ @base_release.present? or raise Internal, "Release #{name} is not present"
41
+ @base_release
42
+ end
43
+
44
+ # List of features in this build. This requires the build to be present on disk
45
+ def features()
46
+ present? or raise "Build #{name} is not present"
47
+ version.zero? ? [] : migration.feature_versions.map { |version| project[version] }
48
+ end
49
+
50
+ # Return the build's history as a hash from release to list of features
51
+ def history()
52
+ h = {}
53
+ Algorithm.follow(self, :base_release).each { |release|
54
+ h[release] = []
55
+ indent {
56
+ release.features.each { |feature| h[release] << feature }
57
+ }
58
+ }
59
+ h
60
+ end
61
+
62
+ def initialize(project, base_release, version, migration, database: nil, schema: nil)
63
+ base_release.nil? || base_release.is_a?(Release) or
64
+ raise Internal, "Expected a Release object, got #{base_release.class}"
65
+ version.is_a?(Version) or raise Internal, "Expected a Version object, got #{version.class}"
66
+ @project = project
67
+ @base_release = base_release
68
+ @version = version
69
+ @migration = migration
70
+ @schema = Schema.new(project)
71
+ @database = database || Database.new("#{project.name}-#{name}", project.user)
72
+ project[name] = self
73
+ end
74
+
75
+ # Return true if the build exists as a branch in git
76
+ def exist?() Git.branch?(name) end
77
+
78
+ # Create and checkout the branch
79
+ def create()
80
+ !present? or raise Fail, "Build #{name} is already present on disk"
81
+ !exist? or raise Fail, "Build #{name} is already present in git"
82
+ Git.create_branch(name)
83
+ Git.checkout_branch(name)
84
+ end
85
+
86
+ # FIXME: Kills the current branch under the feets of the application. Also doesn't update
87
+ # internal structures in Project
88
+ def destroy()
89
+ project.release != self or raise Error, "Can't destroy current branch - #{self.version}"
90
+ Git.delete_branch(name)
91
+ end
92
+
93
+ # True if the release is present in this git branch
94
+ def present?() File.exist?(node) end
95
+
96
+ # True if the release is the active branch
97
+ def active?() Git.current_branch == name end
98
+
99
+ def checkout()
100
+ Git.checkout(name)
101
+ end
102
+
103
+ def checkback() # Doubtfull - creates strange results on Release and Prerelease branches
104
+ base_release.checkout
105
+ end
106
+
107
+ def built?()
108
+ active? && @schema.built?(@database)
109
+ end
110
+
111
+ def build()
112
+ active? or raise Error, "Can't build: Not active"
113
+ @schema.build(@database)
114
+ end
115
+
116
+ def rebuild()
117
+ active? or raise Error, "Can't rebuild: Not active"
118
+ @database.recreate
119
+ build
120
+ end
121
+
122
+ def include_feature(feature)
123
+ migration.include_feature(feature.migration)
124
+ end
125
+
126
+ def remove_feature(feature)
127
+ raise NotYet
128
+ end
129
+
130
+ # Create a copy of the project in tmp/ and checkout the branch. Used to build
131
+ # releases. Returns the path to the copy
132
+ def snapshot() end
133
+
134
+ # Sorting
135
+ def <=>(other) version <=> other.version end
136
+
137
+ # Use #name for String conversion
138
+ def to_s() name end
139
+
140
+ # Reads the name of the base release from a node (see Build#node)
141
+ def self.deref_node_file(node)
142
+ if File.basename(node) == "0.0.0"
143
+ nil
144
+ elsif File.symlink?(node) # Releases and prereleases
145
+ symlink = Command.command("readlink -v #{node}").first
146
+ value = File.basename(symlink.sub(/\/$/, ""))
147
+ value == "/dev/null" ? nil : value
148
+ elsif File.directory?(node) # Migrations
149
+ name = File.basename(node)
150
+ name =~ MIGRATION_RE or raise "Illegal migration name: #{name}"
151
+ $1
152
+ end
153
+ end
154
+
155
+ private
156
+ @states = {
157
+ exist: [:create, :destroy],
158
+ initialized: [:exist, :initialize, false], # ???
159
+ active: [:exist, :checkout, :checkback],
160
+ # built: [:active, :build, lambda { |this| this.database.recreate } ],
161
+ }
162
+ end
163
+
164
+ class AbstractRelease < Build
165
+ # Tag
166
+ def tag() [version.custom, "v#{version.semver}"].compact.join("-") end
167
+
168
+ # Cache object
169
+ attr_reader :archive
170
+
171
+ # Redefine Build#node
172
+ attr_reader :node
173
+
174
+ # The directory representing this release. It is initially empty. It is the
175
+ # same as the next release's migration directory and contains features that
176
+ # require this release
177
+ def release_dir() raise AbstractMethod end
178
+
179
+ def initialize(project, base_release, version, migration, **opts)
180
+ super
181
+ @node = File.join(RELEASE_DIR, name)
182
+ @archive = DumpFile.new(project, version.to_s)
183
+ end
184
+
185
+ def cached?() archive.exist? end
186
+ def cache() database.save(archive.path) end
187
+ def uncache() FileUtils.rm_f(archive.path) end
188
+
189
+ def loaded?() schema.loaded?(database) end
190
+ def load() database.ensure(:loaded, archive.file) end
191
+ def unload() database.ensure(:loaded, expect: false) end
192
+
193
+ def prepare(commit: true)
194
+ migration.prepare
195
+ Git.commit("Prepared next release") if commit
196
+ end
197
+
198
+ def <=>(other) version <=> other.version end
199
+
200
+ # Create the release in Git and on the disk
201
+ def create(create_release_link_file: true)
202
+ super()
203
+
204
+ # Create release link file (eg. releases/0.1.0)
205
+ if create_release_link_file
206
+ base_release_dir = (version.zero? ? "/dev/null" : "../#{migration.path}")
207
+ Dir.chdir(RELEASE_DIR) {
208
+ FileUtils.ln_s(base_release_dir, File.basename(node))
209
+ }
210
+ Git.add(node)
211
+ end
212
+
213
+ # Set schema version
214
+ project.schema.version = version
215
+ Git.add(project.schema.path)
216
+ end
217
+
218
+ def dump
219
+ return self
220
+ $stderr.puts "#{self.class} #{version}"
221
+ $stderr.indent { |f|
222
+ f.puts "node : #{node.inspect}"
223
+ f.puts "base_release: #{base_release&.version.inspect}"
224
+ f.puts "migration : #{migration&.path.inspect}"
225
+ f.puts "release_dir : #{release_dir.inspect}"
226
+ }
227
+ self
228
+ end
229
+
230
+ private
231
+ @ensure_states = {
232
+ # cached: [:built, :cache, :uncache],
233
+ loaded: [:cached, :load, :unload]
234
+ }
235
+ end
236
+
237
+ class Release < AbstractRelease
238
+ def release_dir() File.join(FEATURE_DIR, name) end
239
+
240
+ def initialize(project, base_release, version)
241
+ migration = base_release && ReleaseMigration.new(base_release.release_dir)
242
+ super(project, base_release, version, migration)
243
+ end
244
+
245
+ # Create the release in Git and on the disk. We assume that the migration exists
246
+ def create
247
+ super
248
+
249
+ # Create migration link file
250
+ if !version.zero?
251
+ migration_version = "#{base_release.version}_#{version}"
252
+ features = "../#{migration.path}"
253
+ Dir.chdir(MIGRATION_DIR) {
254
+ FileUtils.ln_s(features, migration_version)
255
+ Git.add(migration_version)
256
+ }
257
+ end
258
+
259
+ # Create new empty feature directory
260
+ ReleaseMigration.new(release_dir).create
261
+
262
+ Git.commit("Release #{version}")
263
+ Git.create_tag(version)
264
+ dump
265
+ migration.dump if migration
266
+ self
267
+ end
268
+ end
269
+
270
+ # TODO: Rename to PreRelease
271
+ class Prerelease < AbstractRelease
272
+ attr_reader :target_release
273
+
274
+ def release_dir() base_release.release_dir end
275
+
276
+ def initialize(project, base_release, version, target_version = version.truncate(:pre))
277
+ @target_release = Release.new(project, base_release, target_version)
278
+ migration = ReleaseMigration.new(target_release.migration.path)
279
+ super(project, base_release, version, migration)
280
+ end
281
+
282
+ # Create the pre-release in Git and on disk
283
+ def create
284
+ super
285
+ migration.prepare
286
+ Git.commit("Pre-release #{version}")
287
+ dump
288
+ self
289
+ end
290
+
291
+ # Create a migration for this release
292
+ def prepare_migration
293
+ base_release.built? or raise "Base release #{base_release} is not built"
294
+ puts "Prerelease#generate_migration"
295
+ end
296
+ end
297
+
298
+ class MigrationPrerelease < Prerelease
299
+ def initialize(project, base_release, version, target_version = version.truncate(:pre))
300
+ release_dir = "/migrations/..."
301
+ end
302
+ end
303
+
304
+ class MigrationRelease < AbstractRelease
305
+ def create
306
+ # does not call super because migrations belong to the release they migrate to
307
+ #
308
+ # code, code, code
309
+ end
310
+
311
+ def include_feature
312
+ raise NotYet
313
+ end
314
+ end
315
+
316
+ class Feature < Build
317
+ # Name of feature
318
+ def feature() version.feature end
319
+
320
+ # A feature's node is the feature directory
321
+ def node() release_dir end
322
+
323
+ def release_dir() migration.path end
324
+
325
+ def initialize(project, base_release, name)
326
+ base_release.is_a?(Release) || base_release.is_a?(Prerelease) or
327
+ raise Internal, "Expected a Release object, got #{base_release.class}"
328
+ version = Version.new(base_release.version, feature: name)
329
+ migration = FeatureMigration.new(Migration.path(version))
330
+ super(project, base_release, version, migration)
331
+ end
332
+
333
+ def checkout()
334
+ super
335
+ # FileUtils.ln_sf(feature, "feature")
336
+ end
337
+
338
+ def create
339
+ super
340
+ migration.create
341
+ migration.prepare
342
+ Git.commit("Created feature #{feature}")
343
+ migration.dump
344
+ self
345
+ end
346
+
347
+ # features/
348
+ # 0.0.0/
349
+ # feature_a
350
+ # feature_b/
351
+ # base_release.prick
352
+ # name.yml <- 0.2.0/feature_b
353
+ # rebased.yml <- reads base_release here
354
+ #
355
+ # 0.2.0/
356
+ # feature_a -> 0.0.0/feature_a <- not rebased
357
+ # feature_b -> 0.0.0/feature_b <- rebased
358
+
359
+
360
+ def rebase(new_base_release)
361
+ # Checkout new_base_release
362
+ # Merge feature
363
+ # Establish symlinks
364
+ # Create as branch
365
+
366
+ # new_base > base_release or
367
+ # raise Error, "Can't rebase from #{base_release.version} to #{new_base.version}"
368
+ # new_feature = Feature.new(project, base_release, base.version, base: base)
369
+ # new_feature.ensure(:active)
370
+ # schema.version = version
371
+ # FileUtils.ln_sf("../#{feature.release_dir}", new_feature.release_dir)
372
+ # Git.add(new_feature.release_dir)
373
+ # new_feature
374
+ end
375
+ end
376
+ end