prick 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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