prick 0.2.0 → 0.7.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.
- checksums.yaml +4 -4
- data/.gitignore +6 -5
- data/Gemfile +4 -1
- data/TODO +10 -0
- data/doc/prick.txt +114 -0
- data/exe/prick +328 -402
- data/lib/ext/fileutils.rb +18 -0
- data/lib/ext/forward_method.rb +18 -0
- data/lib/ext/shortest_path.rb +44 -0
- data/lib/prick.rb +20 -10
- data/lib/prick/branch.rb +254 -0
- data/lib/prick/builder.rb +164 -0
- data/lib/prick/cache.rb +34 -0
- data/lib/prick/command.rb +19 -11
- data/lib/prick/constants.rb +122 -48
- data/lib/prick/database.rb +28 -20
- data/lib/prick/diff.rb +125 -0
- data/lib/prick/exceptions.rb +15 -3
- data/lib/prick/git.rb +77 -30
- data/lib/prick/head.rb +183 -0
- data/lib/prick/migration.rb +40 -200
- data/lib/prick/program.rb +493 -0
- data/lib/prick/project.rb +523 -351
- data/lib/prick/rdbms.rb +4 -13
- data/lib/prick/schema.rb +16 -90
- data/lib/prick/share.rb +64 -0
- data/lib/prick/state.rb +192 -0
- data/lib/prick/version.rb +62 -29
- data/libexec/strip-comments +33 -0
- data/make_releases +48 -345
- data/make_schema +10 -0
- data/prick.gemspec +14 -23
- data/share/diff/diff.after-tables.sql +4 -0
- data/share/diff/diff.before-tables.sql +4 -0
- data/share/diff/diff.tables.sql +8 -0
- data/share/migration/diff.tables.sql +8 -0
- data/share/migration/features.yml +6 -0
- data/share/migration/migrate.sql +3 -0
- data/share/migration/migrate.yml +8 -0
- data/share/migration/tables.sql +3 -0
- data/share/schema/build.yml +14 -0
- data/share/schema/schema.sql +5 -0
- data/share/schema/schema/build.yml +3 -0
- data/share/schema/schema/prick/build.yml +14 -0
- data/share/schema/schema/prick/data.sql +7 -0
- data/share/schema/schema/prick/schema.sql +5 -0
- data/share/{schemas/prick/schema.sql → schema/schema/prick/tables.sql} +2 -5
- data/{file → share/schema/schema/public/.keep} +0 -0
- data/share/schema/schema/public/build.yml +14 -0
- data/share/schema/schema/public/schema.sql +3 -0
- data/test_assorted +192 -0
- data/test_feature +112 -0
- data/test_refactor +34 -0
- data/test_single_dev +83 -0
- metadata +43 -68
- data/lib/prick/build.rb +0 -376
- data/lib/prick/migra.rb +0 -22
- data/share/schemas/prick/data.sql +0 -8
data/lib/prick/project.rb
CHANGED
@@ -1,442 +1,614 @@
|
|
1
|
+
require "prick/state.rb"
|
1
2
|
|
2
|
-
require "
|
3
|
-
|
4
|
-
require "prick/archive.rb"
|
5
|
-
require "prick/build.rb"
|
6
|
-
require "prick/migration.rb"
|
7
|
-
require "prick/database.rb"
|
8
|
-
require "prick/schema.rb"
|
9
|
-
require "prick/git.rb"
|
10
|
-
require "prick/migra.rb"
|
11
|
-
|
12
|
-
require 'fileutils'
|
3
|
+
require "tmpdir"
|
13
4
|
|
14
5
|
module Prick
|
15
6
|
class Project
|
16
|
-
# Name of project. Used in database and release names. Defaults to the name
|
17
|
-
# of the current directory
|
18
7
|
attr_reader :name
|
8
|
+
attr_reader :user # Database user
|
9
|
+
attr_reader :head # Current branch/tag
|
10
|
+
attr_reader :schema
|
19
11
|
|
20
|
-
#
|
21
|
-
|
12
|
+
# Return the versioned database or the project database if `version` is nil
|
13
|
+
def database(version = nil)
|
14
|
+
version ? Database.new("#{name}-#{version}", user) : @database
|
15
|
+
end
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# List of all builds
|
34
|
-
def builds() @builds_by_name.values end
|
35
|
-
|
36
|
-
# List of releases ordered from oldest to newest. Custom releases are sorted
|
37
|
-
# after regular releases and alphabetical between themselves
|
38
|
-
attr_reader :releases
|
39
|
-
|
40
|
-
# List of pre-releases
|
41
|
-
attr_reader :prereleases
|
42
|
-
|
43
|
-
# Hash from base-release name to feature. Note that this doesn't follow release history
|
44
|
-
attr_reader :features
|
45
|
-
|
46
|
-
# Ignored and orphaned objects
|
47
|
-
attr_reader :ignored_feature_nodes
|
48
|
-
attr_reader :ignored_release_nodes
|
49
|
-
attr_reader :orphan_feature_nodes
|
50
|
-
attr_reader :orphan_release_nodes
|
51
|
-
attr_reader :orphan_git_branches
|
52
|
-
|
53
|
-
# True if we're on a release branch
|
54
|
-
def release?() release && release.version.pre.nil? && !feature? end
|
55
|
-
|
56
|
-
# True if we're on a pre-release branch
|
57
|
-
def prerelease?() release && !release.version.pre.nil? && !feature? end
|
58
|
-
|
59
|
-
# True if we're on a feature branch
|
60
|
-
def feature?() release && !release.version.feature.nil? end
|
61
|
-
|
62
|
-
# Sorted list of databases associated with a release. Also include the
|
63
|
-
# project database if project. is true. If all: is true, also list
|
64
|
-
# databases that match the project but is without an associated release
|
65
|
-
def databases(all: false, project: true)
|
66
|
-
r = (project ? [database] : [])
|
67
|
-
if all
|
68
|
-
r + Rdbms.list_databases(Prick.database_re(name)).map { |name| Database.new(name, user) }
|
69
|
-
else
|
70
|
-
r + @releases.map(&:database)
|
71
|
-
end
|
17
|
+
attr_reader :cache
|
18
|
+
|
19
|
+
def initialize(name, user, head)
|
20
|
+
@name = name
|
21
|
+
@user = user || ENV['USER']
|
22
|
+
@head = head
|
23
|
+
@schema = Schema.new
|
24
|
+
@database = Database.new(name, user)
|
25
|
+
@cache = Cache.new
|
72
26
|
end
|
73
27
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
@name = name || File.basename(Dir.getwd)
|
79
|
-
@name =~ PROJECT_NAME_RE or raise Error, "Illegal project name: #@name"
|
80
|
-
@user = user || @name
|
81
|
-
@user =~ USER_NAME_RE or raise Error, "Illegal postgres user name: #@user"
|
82
|
-
# Rdbms.ensure_state(:user_exist, @user)
|
83
|
-
Rdbms.create_user(@user) if !Rdbms.exist_user?(@user) # FIXME
|
84
|
-
@schema = Schema.new(self)
|
85
|
-
@database = Database.new(@name, @user)
|
28
|
+
forward_methods :version, :base_version, :@head
|
29
|
+
forward_methods :clean?, :@head
|
30
|
+
forward_methods :tag?, :release_tag?, :migration_tag?, :@head
|
31
|
+
forward_methods :branch?, :release_branch?, :prerelease_branch?, :feature_branch?, :migration_branch?, :@head
|
86
32
|
|
87
|
-
|
88
|
-
|
33
|
+
def self.exist?(directory)
|
34
|
+
File.directory?(directory) && Dir.chdir(directory) { ProjectState.new.exist? }
|
35
|
+
end
|
89
36
|
|
90
|
-
|
91
|
-
|
92
|
-
@features = {}
|
37
|
+
def self.create(name, user, directory)
|
38
|
+
user ||= ENV['USER']
|
93
39
|
|
94
|
-
|
95
|
-
@ignored_release_nodes = []
|
40
|
+
FileUtils.mkdir(directory) if directory != "."
|
96
41
|
|
97
|
-
|
98
|
-
|
42
|
+
Dir.chdir(directory) {
|
43
|
+
# Initialize git instance
|
44
|
+
Git.init
|
99
45
|
|
100
|
-
|
46
|
+
# Create prick version file
|
47
|
+
PrickVersion.new.write(VERSION)
|
101
48
|
|
102
|
-
|
49
|
+
# Create project state file
|
50
|
+
ProjectState.new(name: name, user: user).write
|
51
|
+
|
52
|
+
# Directories
|
53
|
+
FileUtils.mkdir_p(DIRS)
|
54
|
+
DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
|
55
|
+
|
56
|
+
# Copy default gitignore and schema files
|
57
|
+
Share.cp("gitignore", ".gitignore")
|
58
|
+
Share.cp("schema/schema", ".")
|
59
|
+
|
60
|
+
# Add .prick-migration file
|
61
|
+
MigrationState.new(version: Version.zero).create
|
62
|
+
|
63
|
+
# Add everything so far
|
64
|
+
Git.add(".")
|
65
|
+
Git.commit("Initial import")
|
66
|
+
|
67
|
+
# Rename branch "master"/"main" to "0.0.0_initial"
|
68
|
+
from_branch = Git.branch?("master") ? "master" : "main"
|
69
|
+
Git.rename_branch(from_branch, "0.0.0_initial")
|
70
|
+
|
71
|
+
# Create schema
|
72
|
+
schema = Schema.new
|
73
|
+
schema.version = Version.zero
|
74
|
+
Git.add schema.version_file
|
75
|
+
|
76
|
+
# Create release
|
77
|
+
Git.commit "Created release v0.0.0"
|
78
|
+
Git.create_tag(Version.zero)
|
79
|
+
Git.checkout_tag(Version.zero)
|
80
|
+
}
|
103
81
|
end
|
104
82
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
83
|
+
# Initialize from disk
|
84
|
+
#
|
85
|
+
# TODO Handle migrations
|
86
|
+
def self.load
|
87
|
+
if Git.detached?
|
88
|
+
name = "v#{Git.current_tag}"
|
111
89
|
else
|
112
|
-
|
90
|
+
name = Git.current_branch
|
91
|
+
end
|
92
|
+
begin
|
93
|
+
branch = Head.load(name)
|
94
|
+
rescue Version::FormatError
|
95
|
+
raise Fail, "Illegal branch name: #{name}"
|
113
96
|
end
|
97
|
+
state = ProjectState.new.read
|
98
|
+
Project.new(state.name, state.user, branch)
|
114
99
|
end
|
115
100
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
101
|
+
def build(database = self.database, version: nil)
|
102
|
+
database.clean
|
103
|
+
if version
|
104
|
+
FileUtils.mkdir_p(TMP_DIR)
|
105
|
+
Dir.mktmpdir("clone-", TMP_DIR) { |dir|
|
106
|
+
Command.command "git clone . #{dir}"
|
107
|
+
Dir.chdir(dir) {
|
108
|
+
Git.checkout_tag(version)
|
109
|
+
project = Project.load
|
110
|
+
project.head.build(database)
|
111
|
+
}
|
112
|
+
}
|
124
113
|
else
|
125
|
-
|
114
|
+
head.build(database)
|
126
115
|
end
|
127
|
-
|
116
|
+
self
|
128
117
|
end
|
129
118
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
when Version; @builds_by_version.key?(version)
|
137
|
-
when NilClass; nil
|
119
|
+
def load(database = self.database, version: nil, file: nil)
|
120
|
+
version.nil? ^ file.nil? or raise Internal, "Need exactly one of :file and :version to be defined"
|
121
|
+
database.clean
|
122
|
+
if version
|
123
|
+
cache.exist?(version) or raise Internal, "Can't find cache file for database #{database}"
|
124
|
+
cache.load(database, version)
|
138
125
|
else
|
139
|
-
|
126
|
+
database.load(file)
|
140
127
|
end
|
128
|
+
self
|
141
129
|
end
|
142
130
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
|
153
|
-
}
|
131
|
+
def save(database = nil, file: nil)
|
132
|
+
!database.nil? || file or raise Internal, "Need a database when saving to file"
|
133
|
+
database ||= self.database
|
134
|
+
if file
|
135
|
+
database.save(file)
|
136
|
+
else
|
137
|
+
cache.save(database)
|
138
|
+
end
|
139
|
+
self
|
154
140
|
end
|
155
141
|
|
156
|
-
# Create
|
157
|
-
#
|
158
|
-
def
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
142
|
+
# Create a schema diff between two database versions. to_version defaults to the
|
143
|
+
# current version. Returns the Diff object
|
144
|
+
def diff(from_version, to_version)
|
145
|
+
begin
|
146
|
+
from_db = Database.new("#{name}-base", user)
|
147
|
+
to_db = Database.new("#{name}-next", user)
|
148
|
+
from_version ||= version
|
149
|
+
build(from_db, version: from_version)
|
150
|
+
build(to_db, version: to_version)
|
151
|
+
Diff.new(from_db, to_db)
|
152
|
+
ensure
|
153
|
+
from_db&.drop
|
154
|
+
to_db&.drop
|
155
|
+
end
|
156
|
+
end
|
168
157
|
|
169
|
-
|
158
|
+
def prepare_release(fork = nil)
|
159
|
+
check_clean(:release_tag)
|
160
|
+
@head = ReleaseBranch.new(fork, version).create
|
161
|
+
submit "Prepared new release based on v#{version}", true
|
162
|
+
self
|
170
163
|
end
|
171
164
|
|
172
|
-
|
173
|
-
|
165
|
+
def prepare_diff(from_version = version)
|
166
|
+
begin
|
167
|
+
from_name = "#{name}-base"
|
168
|
+
from_db = Database.new(from_name, user)
|
169
|
+
build(from_db, version: from_version)
|
170
|
+
|
171
|
+
to_name = "#{name}-next"
|
172
|
+
to_db = Database.new(to_name, user)
|
173
|
+
build(to_db)
|
174
174
|
|
175
|
-
|
175
|
+
if prerelease_branch?
|
176
|
+
head.migrate_features(from_db)
|
177
|
+
end
|
176
178
|
|
177
|
-
|
178
|
-
|
179
|
-
|
179
|
+
diff = Diff.new(from_db, to_db)
|
180
|
+
for path, lines, tables in [
|
181
|
+
[BEFORE_TABLES_DIFF_PATH, diff.before_table_changes, false],
|
182
|
+
[TABLES_DIFF_PATH, diff.table_changes, true],
|
183
|
+
[AFTER_TABLES_DIFF_PATH, diff.after_table_changes, false]]
|
184
|
+
if lines.empty?
|
185
|
+
if File.exist?(path)
|
186
|
+
if tables
|
187
|
+
Share.cp(File.join("diff", File.basename(path)), path)
|
188
|
+
Git.add(path)
|
189
|
+
else
|
190
|
+
Git.rm(path)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
else
|
194
|
+
Share.cp(File.join("diff", File.basename(path)), path)
|
195
|
+
File.open(path, "a") { |f| f.puts lines }
|
196
|
+
Git.add(path) if !tables
|
197
|
+
end
|
198
|
+
end
|
199
|
+
self
|
200
|
+
ensure
|
201
|
+
from_db&.drop
|
202
|
+
to_db&.drop
|
203
|
+
end
|
180
204
|
end
|
181
205
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
history.each { |release| release.migration.migrate(database.name) }
|
189
|
-
database.version = release.version
|
206
|
+
def create_release(new_version)
|
207
|
+
check_clean(:release_branch)
|
208
|
+
head = ReleaseTag.new(new_version, version)
|
209
|
+
check_migration(head.migration)
|
210
|
+
(@head = head).create
|
211
|
+
self
|
190
212
|
end
|
191
213
|
|
192
|
-
|
214
|
+
def generate_schema
|
215
|
+
build = SchemaBuilder.new(database, SCHEMA_DIR).build(execute: false)
|
216
|
+
puts build.lines
|
217
|
+
end
|
193
218
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
release? or raise Error, "Need to be on a release branch to create a feature"
|
198
|
-
feature = Feature.new(self, release, name).ensure(:active)
|
199
|
-
Git.commit "Created feature #{name}"
|
219
|
+
def generate_migration
|
220
|
+
build = MigrationBuilder.new(database, MIGRATION_DIR).build(execute: false)
|
221
|
+
puts build.lines
|
200
222
|
end
|
201
223
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
224
|
+
private
|
225
|
+
def check(kind) self.send(:"#{kind}?") end
|
226
|
+
def check_clean(kind = nil)
|
227
|
+
clean? or raise Internal, "Dirty repository"
|
228
|
+
kind.nil? || check(kind) or raise Internal, "Not on a #{kind} tag/branh"
|
207
229
|
end
|
208
230
|
|
209
|
-
|
231
|
+
def check_branch() branch? or raise Internal, "Not on a branch" end
|
232
|
+
def check_tag() tag? or raise Internal, "Not on a tag" end
|
210
233
|
|
211
|
-
#
|
212
|
-
#
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
check_clean
|
223
|
-
prerelease? || feature? or raise Error, "Need to be on a prerelease or feature branch to include features"
|
224
|
-
|
225
|
-
base_database = release.base_release.database
|
226
|
-
build(base_release.version) if !base_database.loaded?
|
227
|
-
build # if !database.loaded?
|
228
|
-
|
229
|
-
diff_sql = release.migration.diff_sql
|
230
|
-
Migra.migrate(base_database.name, database.name, diff_sql)
|
231
|
-
end
|
232
|
-
|
233
|
-
def create_release(version = nil)
|
234
|
-
check_clean
|
235
|
-
if release?
|
236
|
-
!version.nil? or raise Error, "Need version argument when on a release branch"
|
237
|
-
new_release = Release.new(self, release, version)
|
238
|
-
elsif prerelease?
|
239
|
-
version.nil? or raise Error, "Can't use version argument when on a pre-release branch"
|
240
|
-
new_release = release.target_release
|
234
|
+
# FIXME: Use Cache::file
|
235
|
+
def cache_file(version) File.join(CACHE_DIR, "#{database(version)}.sql.gz") end
|
236
|
+
|
237
|
+
def submit(msg, commit = true)
|
238
|
+
Git.commit msg if commit
|
239
|
+
@message = msg
|
240
|
+
end
|
241
|
+
|
242
|
+
def clean(database) # FIXME: Use Database#clean
|
243
|
+
if database.exist?
|
244
|
+
database.recreate if database.loaded?
|
241
245
|
else
|
242
|
-
|
246
|
+
database.create
|
243
247
|
end
|
244
|
-
new_release.create
|
245
|
-
new_release
|
246
248
|
end
|
247
249
|
|
248
|
-
def
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
250
|
+
def check_migration(migration)
|
251
|
+
begin
|
252
|
+
from_version = migration.base_version
|
253
|
+
from_db = Database.new("#{name}-base", user)
|
254
|
+
to_db = Database.new("#{name}-next", user)
|
255
|
+
build(from_db, version: from_version)
|
256
|
+
build(to_db)
|
257
|
+
migration.migrate(from_db)
|
258
|
+
Diff.new(from_db, to_db).same? or raise Error, "Schema/migration mismatch"
|
259
|
+
ensure
|
260
|
+
from_db&.drop
|
261
|
+
to_db&.drop
|
262
|
+
end
|
253
263
|
end
|
264
|
+
end
|
265
|
+
end
|
254
266
|
|
255
|
-
|
256
|
-
check_clean
|
257
|
-
prerelease? or raise Error, "Need to be on a pre-release branch to make an incremental pre-release"
|
258
|
-
prerelease = Prerelease.new(self, release.base_release, release.version.increment(:pre))
|
259
|
-
prerelease.create
|
260
|
-
end
|
267
|
+
__END__
|
261
268
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
feature.create
|
267
|
-
end
|
269
|
+
module Prick
|
270
|
+
class Project
|
271
|
+
# Name of project. Persisted in the project state file
|
272
|
+
attr_reader :name
|
268
273
|
|
269
|
-
#
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
end
|
274
|
+
# Name of Postgresql user that owns the databases. Defaults to #name.
|
275
|
+
# Persisted in the project state file. TODO: Make writable
|
276
|
+
attr_reader :user
|
277
|
+
|
278
|
+
# Current branch
|
279
|
+
attr :branch
|
276
280
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
#
|
281
|
+
# Version of the current branch. If is defined as #branch.version
|
282
|
+
def version() branch.version end
|
283
|
+
|
284
|
+
# Project (default) database
|
285
|
+
def database(version = nil)
|
286
|
+
version ? Database.new("#{name}-#{version.truncate(:pre)}", user) : @database
|
281
287
|
end
|
282
288
|
|
283
|
-
|
289
|
+
# Last commit message. TODO: Move to git.rb
|
290
|
+
attr_reader :message
|
291
|
+
|
292
|
+
# True if we're on a tag
|
293
|
+
def tag?() Git.detached? end
|
294
|
+
def self.tag?() Git.detached? end
|
295
|
+
|
296
|
+
# Classifiers
|
297
|
+
forward_methods :release?, :prerelease?, :feature?, :migration?, :@branch
|
298
|
+
|
299
|
+
def initialize(name, user, branch)
|
300
|
+
@name = name
|
301
|
+
@user = user
|
302
|
+
@branch = branch
|
303
|
+
@database = Database.new(name, user)
|
284
304
|
end
|
285
305
|
|
286
|
-
|
287
|
-
|
288
|
-
if
|
289
|
-
|
290
|
-
schema.build
|
306
|
+
def self.load
|
307
|
+
name = Git.current_branch || Git.current_tag
|
308
|
+
if name =~ MIGRATION_RE
|
309
|
+
branch = MigrationRelease.load(name)
|
291
310
|
else
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
311
|
+
begin
|
312
|
+
version = Version.new(name)
|
313
|
+
rescue Version::FormatError
|
314
|
+
raise Fail, "Illegal branch name: #{name}"
|
315
|
+
end
|
316
|
+
if version.release?
|
317
|
+
branch = Release.load(name)
|
318
|
+
elsif version.pre?
|
319
|
+
branch = PreRelease.load(name, version)
|
320
|
+
elsif version.feature?
|
321
|
+
branch = Feature.load(name)
|
322
|
+
else
|
323
|
+
raise Oops
|
298
324
|
end
|
299
|
-
Dir.chdir(CLONE_DIR) {
|
300
|
-
Git.checkout_tag(version.to_s)
|
301
|
-
project = Project.new(name, user)
|
302
|
-
release.database.recreate
|
303
|
-
project.schema.build(release.database)
|
304
|
-
}
|
305
|
-
release.cache
|
306
325
|
end
|
326
|
+
state = ProjectState.new.read
|
327
|
+
Project.new(state.name, state.user, branch)
|
307
328
|
end
|
308
329
|
|
309
|
-
|
310
|
-
|
311
|
-
check_clean
|
312
|
-
File.file?(file) or raise Error, "Can't find #{file}"
|
313
|
-
database.recreate
|
314
|
-
database.load(file)
|
330
|
+
def self.initialized?(directory)
|
331
|
+
File.directory?(directory) && Dir.chdir(directory) { ProjectState.new.exist? }
|
315
332
|
end
|
316
333
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
334
|
+
def self.initialize_directory(name, user, directory)
|
335
|
+
FileUtils.mkdir_p(directory)
|
336
|
+
Dir.chdir(directory) {
|
337
|
+
# Initialize git instance
|
338
|
+
Git.init
|
339
|
+
|
340
|
+
# Create prick version file
|
341
|
+
PrickVersion.new.write(VERSION)
|
342
|
+
|
343
|
+
# Create project state file
|
344
|
+
ProjectState.new(name: name, user: user).write
|
345
|
+
|
346
|
+
# Directories
|
347
|
+
DIRS.each { |dir|
|
348
|
+
!File.exist?(dir) or raise Fail, "Already initialized: Directory '#{dir}' exists"
|
349
|
+
}
|
350
|
+
FileUtils.mkdir_p(DIRS)
|
351
|
+
DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
|
352
|
+
|
353
|
+
# Copy default gitignore and schema files
|
354
|
+
Share.cp("gitignore", ".gitignore")
|
355
|
+
Share.cp("schemas", ".")
|
356
|
+
|
357
|
+
# Add everything so far
|
358
|
+
Git.add(".")
|
359
|
+
Git.commit("Initial import")
|
360
|
+
|
361
|
+
# Create initial release
|
362
|
+
release = Release.new(Version.zero, nil)
|
363
|
+
release.create
|
364
|
+
|
365
|
+
schema = Schema.new(SCHEMAS_DIR)
|
366
|
+
schema.version = release.version
|
367
|
+
Git.add schema.version_file
|
368
|
+
|
369
|
+
Git.commit "Release 0.0.0"
|
370
|
+
release.tag!
|
371
|
+
|
372
|
+
# Kill master branch
|
373
|
+
Git.delete_branch("master")
|
374
|
+
}
|
333
375
|
end
|
334
376
|
|
335
|
-
|
336
|
-
|
337
|
-
# end
|
338
|
-
#
|
339
|
-
# def unload(version = nil)
|
340
|
-
# if version
|
341
|
-
# lookup(version).database.drop
|
342
|
-
# else
|
343
|
-
# databases.each(&:drop)
|
344
|
-
# end
|
345
|
-
# end
|
346
|
-
#
|
347
|
-
# Remove temporary files and databases
|
348
|
-
def cleanup(version = nil, project: false)
|
377
|
+
def build(database: self.database, version: nil)
|
378
|
+
clean(database)
|
349
379
|
if version
|
350
|
-
|
351
|
-
|
352
|
-
|
380
|
+
FileUtils.mkdir_p(TMP_DIR)
|
381
|
+
Dir.mktmpdir("clone-", TMP_DIR) { |dir|
|
382
|
+
Command.command "git clone . #{dir}"
|
383
|
+
Dir.chdir(dir) {
|
384
|
+
Git.checkout_tag(version)
|
385
|
+
project = Project.load
|
386
|
+
project.branch.build(database)
|
387
|
+
}
|
388
|
+
}
|
353
389
|
else
|
354
|
-
|
355
|
-
FileUtils::rm_f Dir.glob(File.join(CACHE_DIR, Prick.dump_glob(name)))
|
356
|
-
databases(all: true, project: project).each(&:drop)
|
390
|
+
branch.build(database)
|
357
391
|
end
|
392
|
+
self
|
358
393
|
end
|
359
394
|
|
360
|
-
|
361
|
-
|
362
|
-
|
395
|
+
def load(file, database: self.database)
|
396
|
+
clean(database)
|
397
|
+
database.load(file)
|
398
|
+
self
|
363
399
|
end
|
364
400
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
puts " #{k} -> #{v || 'nil'}"
|
369
|
-
}
|
401
|
+
def save(file, database: self.database)
|
402
|
+
database.save(file)
|
403
|
+
self
|
370
404
|
end
|
371
405
|
|
372
|
-
def
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
406
|
+
def cache_file(version) File.join(CACHE_DIR, "#{database(version)}.sql.gz") end
|
407
|
+
|
408
|
+
def make(database, subject)
|
409
|
+
Schema.built? or raise Error, "Schema is not built into project database"
|
410
|
+
Schema.make(database, subject)
|
411
|
+
end
|
412
|
+
|
413
|
+
def backup(path = nil)
|
414
|
+
save path || File.join(SPOOL_DIR, "#{name}-#{Time.now.utc.strftime("%Y%m%d-%H%M%S")}.sql.gz")
|
415
|
+
end
|
416
|
+
|
417
|
+
def restore(path = nil)
|
418
|
+
load path || Dir.glob(File.join(SPOOL_DIR, "#{name}-*.sql.gz")).sort.last
|
419
|
+
end
|
420
|
+
|
421
|
+
def prepare_release(commit: true)
|
422
|
+
release = ReleaseMigration.new(nil, branch.version).create
|
423
|
+
submit "Prepared new release based on #{version}", commit
|
424
|
+
release
|
425
|
+
end
|
426
|
+
|
427
|
+
def prepare_schema(name, commit: true)
|
428
|
+
path = File.join(SCHEMAS_DIR, name)
|
429
|
+
FileUtils.mkdir_p(path)
|
430
|
+
Git.add Share.cp("schema/*", path, clobber: false, templates: { 'SCHEMA' => name })
|
431
|
+
File.open(branch.schema.yml_file, "a") { |f| f.write("- #{name}\n") }
|
432
|
+
Git.add(branch.schema.yml_file)
|
433
|
+
submit "Added schema #{name}", commit
|
434
|
+
end
|
435
|
+
|
436
|
+
def prepare_diff(from_version)
|
437
|
+
to_database = nil
|
438
|
+
begin
|
439
|
+
from_name = "#{name}-base"
|
440
|
+
from_database = Database.new(from_name, user)
|
441
|
+
build(database: from_database, version: from_version)
|
442
|
+
|
443
|
+
to_name = "#{name}-next"
|
444
|
+
to_database = Database.new(to_name, user)
|
445
|
+
build(database: to_database)
|
446
|
+
|
447
|
+
# Helpful double-check
|
448
|
+
Diff.same?(to_database.name, database.name) or
|
449
|
+
raise Error, "Schema and project database are not synchronized"
|
450
|
+
|
451
|
+
if release?
|
452
|
+
dir = branch.migration.migration_dir
|
391
453
|
else
|
392
|
-
|
454
|
+
dir = branch.migration.features_dir
|
393
455
|
end
|
394
|
-
}
|
395
456
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
if
|
400
|
-
|
401
|
-
if releases.key?(version)
|
402
|
-
base_release = Build.deref_node_file(File.join(RELEASE_DIR, node))
|
403
|
-
releases[version] = base_release
|
404
|
-
elsif prereleases.key?(version)
|
405
|
-
base_release = Build.deref_node_file(File.join(RELEASE_DIR, node))
|
406
|
-
prereleases[version] = base_release
|
407
|
-
else
|
408
|
-
@orphan_release_nodes << node
|
409
|
-
end
|
410
|
-
else !prerelease_deps.key?(node)
|
411
|
-
@ignored_release_nodes << node
|
457
|
+
# dir = branch.migration.features_dir || branch.migration.migration_dir
|
458
|
+
to_file = File.join(dir, DIFF_FILE)
|
459
|
+
|
460
|
+
if prerelease?
|
461
|
+
branch.migrate_features(from_database)
|
412
462
|
end
|
463
|
+
|
464
|
+
Diff.new(from_name, to_name).write(to_file)
|
465
|
+
ensure
|
466
|
+
from_database&.drop
|
467
|
+
to_database&.drop
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def prepare_migration(from, commit: true)
|
472
|
+
to = branch.version
|
473
|
+
migration_release = MigrationRelease.new(to, from)
|
474
|
+
migration_release.create
|
475
|
+
submit "Prepared migration from #{from} to #{to}", commit
|
476
|
+
migration_release
|
477
|
+
end
|
478
|
+
|
479
|
+
def create_release(version, commit: true)
|
480
|
+
check_migration(branch.version) or raise Error, "Schema/migration mismatch"
|
481
|
+
release = Release.new(version, branch.version)
|
482
|
+
release.create
|
483
|
+
submit "Created release #{version}", commit
|
484
|
+
release.tag!
|
485
|
+
build
|
486
|
+
release
|
487
|
+
end
|
488
|
+
|
489
|
+
def cancel_release(version, commit: true)
|
490
|
+
Git.cancel_tag(version)
|
491
|
+
end
|
492
|
+
|
493
|
+
def create_release_from_prerelease(commit: true)
|
494
|
+
check_migration(branch.base_version) or raise Error, "Schema/migration mismatch"
|
495
|
+
release = Release.new(branch.version.truncate(:pre), branch.base_version)
|
496
|
+
release.create
|
497
|
+
submit "Released #{release.version}", commit
|
498
|
+
release.tag!
|
499
|
+
build
|
500
|
+
release
|
501
|
+
end
|
502
|
+
|
503
|
+
def create_prerelease(version, commit: true)
|
504
|
+
prerelease = PreRelease.new(version.increment(:pre), branch.version)
|
505
|
+
prerelease.create
|
506
|
+
submit "Created pre-release #{prerelease.version}", commit
|
507
|
+
build
|
508
|
+
prerelease
|
509
|
+
end
|
510
|
+
|
511
|
+
def increment_prerelease(commit: true)
|
512
|
+
prerelease = branch.increment
|
513
|
+
prerelease.create
|
514
|
+
submit "Created pre-release #{prerelease.version}", commit
|
515
|
+
build
|
516
|
+
prerelease
|
517
|
+
end
|
518
|
+
|
519
|
+
# # Turned out to be a bad idea
|
520
|
+
# def retarget_migration(version, commit: true)
|
521
|
+
# branch.retarget(version)
|
522
|
+
# commit "Retargeted to #{version}" if commit
|
523
|
+
# branch
|
524
|
+
# end
|
525
|
+
|
526
|
+
def create_feature(name, commit: true)
|
527
|
+
feature = Feature.new(name, version)
|
528
|
+
feature.create
|
529
|
+
submit "Created feature #{name}", commit
|
530
|
+
end
|
531
|
+
|
532
|
+
def include_feature(feature_version, commit: false)
|
533
|
+
Git.merge_branch(feature_version, exclude_files: [branch.schema.version_file], fail: true)
|
534
|
+
branch.include(feature_version)
|
535
|
+
end
|
536
|
+
|
537
|
+
# Checks that the migration of the current branch takes the database from
|
538
|
+
# the base version to the content of the schema
|
539
|
+
def check_migration(from_version)
|
540
|
+
begin
|
541
|
+
from_db = Database.new("#{name}-base", user)
|
542
|
+
to_db = Database.new("#{name}-next", user)
|
543
|
+
build(database: from_db, version: from_version)
|
544
|
+
build(database: to_db)
|
545
|
+
migration = # FIXME: This may be a repeated pattern
|
546
|
+
if migration?
|
547
|
+
branch.migration
|
548
|
+
elsif prerelease?
|
549
|
+
branch.migration
|
550
|
+
else
|
551
|
+
Migration.new(nil, branch.directory)
|
552
|
+
end
|
553
|
+
migration.migrate(from_db)
|
554
|
+
Diff.new(from_db, to_db).same?
|
555
|
+
ensure
|
556
|
+
from_db&.drop
|
557
|
+
to_db&.drop
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# TODO: Make production-only and add a to-argument
|
562
|
+
def upgrade
|
563
|
+
branches = list_upgrades(database.version, branch.version)
|
564
|
+
FileUtils.mkdir_p(TMP_DIR)
|
565
|
+
Dir.mktmpdir("clone-", TMP_DIR) { |dir|
|
566
|
+
Command.command "git clone . #{dir}"
|
567
|
+
Dir.chdir(dir) {
|
568
|
+
branches.each { |branch|
|
569
|
+
branch.checkout_release
|
570
|
+
project = Project.load
|
571
|
+
project.branch.migrate(project.database)
|
572
|
+
}
|
573
|
+
}
|
413
574
|
}
|
575
|
+
end
|
414
576
|
|
415
|
-
|
416
|
-
|
577
|
+
def list_releases(all: false)
|
578
|
+
Git.list_tags(include_cancelled: all).grep(RELEASE_RE) { |tag|
|
579
|
+
version = $2
|
580
|
+
dir = File.join(RELEASES_DIR, version)
|
581
|
+
migration_state = MigrationState.new(dir).read(tag: tag)
|
582
|
+
Release.new(migration_state.version, migration_state.base_version)
|
417
583
|
}
|
418
|
-
|
584
|
+
end
|
419
585
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
586
|
+
# TODO
|
587
|
+
# list_migrations(all: false)
|
588
|
+
def list_migrations
|
589
|
+
Git.list_branches.grep(MIGRATION_RE) { |branch|
|
590
|
+
from_version = Version.new($1)
|
591
|
+
to_version = Version.new($4)
|
592
|
+
MigrationRelease.new(to_version, from_version)
|
426
593
|
}
|
427
|
-
|
594
|
+
end
|
428
595
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
596
|
+
def list_upgrades(from, to)
|
597
|
+
edges = (list_releases + list_migrations).map { |branch| [branch.base_version, branch.version, branch] }
|
598
|
+
(Algorithm.shortest_path(edges, from, to) || []).map(&:last)
|
599
|
+
end
|
433
600
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
601
|
+
private
|
602
|
+
def submit(msg, commit = true)
|
603
|
+
Git.commit msg if commit
|
604
|
+
@message = msg
|
605
|
+
end
|
606
|
+
|
607
|
+
def clean(database)
|
608
|
+
if database.exist?
|
609
|
+
database.recreate if database.loaded?
|
610
|
+
else
|
611
|
+
database.create
|
440
612
|
end
|
441
613
|
end
|
442
614
|
end
|