prick 0.2.0 → 0.3.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 +3 -5
- data/Gemfile +4 -1
- data/TODO +3 -0
- data/doc/prick.txt +114 -0
- data/exe/prick +224 -370
- data/lib/ext/fileutils.rb +11 -0
- data/lib/ext/forward_method.rb +18 -0
- data/lib/ext/shortest_path.rb +44 -0
- data/lib/prick.rb +17 -9
- data/lib/prick/branch.rb +254 -0
- data/lib/prick/builder.rb +141 -0
- data/lib/prick/command.rb +19 -11
- data/lib/prick/constants.rb +42 -20
- data/lib/prick/database.rb +5 -3
- data/lib/prick/diff.rb +47 -0
- data/lib/prick/exceptions.rb +15 -3
- data/lib/prick/git.rb +46 -21
- data/lib/prick/migration.rb +165 -185
- data/lib/prick/program.rb +238 -0
- data/lib/prick/project.rb +266 -358
- data/lib/prick/rdbms.rb +2 -2
- data/lib/prick/schema.rb +19 -88
- data/lib/prick/share.rb +64 -0
- data/lib/prick/state.rb +137 -0
- data/lib/prick/version.rb +34 -14
- data/libexec/strip-comments +33 -0
- data/make_releases +48 -345
- data/make_schema +10 -0
- data/prick.gemspec +11 -22
- data/share/feature_migration/diff.sql +2 -0
- data/share/feature_migration/migrate.sql +2 -0
- data/share/release_migration/diff.sql +3 -0
- data/share/release_migration/features.yml +6 -0
- data/share/release_migration/migrate.sql +5 -0
- data/share/release_migration/migrate.yml +5 -0
- data/share/schema/build.yml +14 -0
- data/share/schema/schema.sql +5 -0
- data/share/schemas/build.yml +3 -0
- data/share/schemas/prick/build.yml +14 -0
- data/share/schemas/prick/data.sql +1 -2
- data/share/schemas/prick/schema.sql +0 -15
- data/share/schemas/prick/tables.sql +17 -0
- data/share/schemas/public/.keep +0 -0
- data/share/schemas/public/build.yml +14 -0
- data/share/schemas/public/schema.sql +3 -0
- data/test_assorted +192 -0
- data/test_feature +112 -0
- data/test_single_dev +83 -0
- metadata +34 -61
@@ -0,0 +1,238 @@
|
|
1
|
+
|
2
|
+
require "prick.rb"
|
3
|
+
|
4
|
+
module Prick
|
5
|
+
# Implements the command line commands
|
6
|
+
class Program
|
7
|
+
def project() @project ||= Project.load end
|
8
|
+
|
9
|
+
attr_accessor :quiet
|
10
|
+
attr_accessor :verbose
|
11
|
+
|
12
|
+
def initialize(quiet: false, verbose: false)
|
13
|
+
@quiet = quiet
|
14
|
+
@verbose = verbose
|
15
|
+
end
|
16
|
+
|
17
|
+
def mesg(*args) puts args.compact.grep(/\S/).join(' ') if !quiet end
|
18
|
+
def verb(*args) puts args.compact.grep(/\S/).join(' ') if verbose end
|
19
|
+
def check_clean() Git.clean? or raise Error, "Repository is dirty - please commit your changes first" end
|
20
|
+
|
21
|
+
def initialize_directory(project_name, database_user, directory)
|
22
|
+
!Project.initialized?(directory) or raise Error, "Directory #{directory} is already initialized"
|
23
|
+
Project.initialize_directory(project_name, database_user, directory)
|
24
|
+
if project_name != File.basename(directory)
|
25
|
+
mesg "Initialized project #{project_name} in #{directory}"
|
26
|
+
else
|
27
|
+
mesg "Initialized project #{project_name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def info
|
32
|
+
if project.tag?
|
33
|
+
puts "At v#{project.version} tag"
|
34
|
+
else
|
35
|
+
puts "On branch #{project.branch.name}"
|
36
|
+
end
|
37
|
+
puts " Git is " + (Git.clean? ? "clean" : "dirty")
|
38
|
+
bv = project.branch.version
|
39
|
+
dv = project.database.version
|
40
|
+
sv = project.branch.schema.version
|
41
|
+
puts " Database version: #{dv}" + (dv != bv ? " (mismatch)" : "")
|
42
|
+
puts " Schema version : #{sv}" + (sv != bv ? " (mismatch)" : "")
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: Move to project to take advantage of cache
|
46
|
+
def build(database, version, no_cache)
|
47
|
+
version = version && Version.new(version)
|
48
|
+
into_mesg = database && "into #{database}"
|
49
|
+
database = database ? Database.new(database, project.user) : project.database(version)
|
50
|
+
if version
|
51
|
+
Git.tag?(version) or raise Error, "Can't find tag v#{version}"
|
52
|
+
cache_file = project.cache_file(version)
|
53
|
+
if !no_cache && File.exist?(cache_file)
|
54
|
+
project.load(cache_file, database: database)
|
55
|
+
mesg "Loaded v#{version}", into_mesg, "from cache"
|
56
|
+
else
|
57
|
+
project.build(database: database, version: version)
|
58
|
+
project.save(cache_file, database: database)
|
59
|
+
mesg "Built v#{version}", into_mesg
|
60
|
+
end
|
61
|
+
else
|
62
|
+
project.build(database: database)
|
63
|
+
mesg "Built current schema", into_mesg
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def load(database, file_or_version)
|
68
|
+
version = Version.try(file_or_version)
|
69
|
+
into_mesg = database && "into #{database}"
|
70
|
+
database = database ? Database.new(database, project.user) : project.database(version)
|
71
|
+
if version
|
72
|
+
file = project.cache_file(version)
|
73
|
+
File.exist?(file) or raise Error, "Can't find #{file} - forgot to build?"
|
74
|
+
project.load(file, database: database)
|
75
|
+
mesg "Loaded v#{version}", into_mesg
|
76
|
+
else
|
77
|
+
file = file_or_version
|
78
|
+
project.load(file, database: database)
|
79
|
+
mesg "Loaded #{file}", into_mesg
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def save(database, file)
|
84
|
+
file ||= "#{ENV['USER']}-#{name}-#{branch}.sql.gz"
|
85
|
+
subject_mesg = database ? "database #{database}" : "current database"
|
86
|
+
database = database ? Database.new(database, project.user) : project.database(version)
|
87
|
+
project.save(file, database: database)
|
88
|
+
mesg "Saved", subject_mesg, "to #{file}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def make(subject)
|
92
|
+
project.database.exist? or raise Error, "Project database is not present"
|
93
|
+
project.make(project.database, subject)
|
94
|
+
end
|
95
|
+
|
96
|
+
def list_releases(migrations: false, cancelled: false)
|
97
|
+
puts (project.list_releases(all: cancelled) + (migrations ? project.list_migrations : [])).sort.map(&:name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def list_migrations
|
101
|
+
puts project.list_migrations.sort.map(&:name)
|
102
|
+
end
|
103
|
+
|
104
|
+
def list_upgrades(from = nil, to = nil)
|
105
|
+
from = from ? Version.new(from) : project.database.version
|
106
|
+
to = to ? Version.new(to) : project.branch.version
|
107
|
+
branches = project.list_upgrades(from, to)
|
108
|
+
puts branches.map(&:name)
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare_schema(name)
|
112
|
+
project.prepare_schema(name)
|
113
|
+
mesg project.message
|
114
|
+
end
|
115
|
+
|
116
|
+
def prepare_diff(version = nil)
|
117
|
+
version ||=
|
118
|
+
if project.prerelease? || project.migration? || project.feature?
|
119
|
+
project.branch.base_version
|
120
|
+
else
|
121
|
+
project.branch.version
|
122
|
+
end
|
123
|
+
project.prepare_diff(version)
|
124
|
+
mesg "Remember to update the associated SQL migration files"
|
125
|
+
end
|
126
|
+
|
127
|
+
def prepare_release
|
128
|
+
check_clean
|
129
|
+
project.version.release? or raise Error, "You need to be on a release branch to prepare a release"
|
130
|
+
project.prepare_release
|
131
|
+
mesg project.message
|
132
|
+
end
|
133
|
+
|
134
|
+
def check
|
135
|
+
version ||=
|
136
|
+
if project.prerelease? || project.migration?
|
137
|
+
project.branch.base_version
|
138
|
+
else
|
139
|
+
project.branch.version
|
140
|
+
end
|
141
|
+
project.check_migration(version)
|
142
|
+
end
|
143
|
+
|
144
|
+
# `arg` can be a version numer of a relative increase (eg. 'minor')
|
145
|
+
def create_release(arg = nil)
|
146
|
+
check_clean
|
147
|
+
if project.release?
|
148
|
+
arg or raise Error, "Need a version argument"
|
149
|
+
version = compute_version(project.version, arg)
|
150
|
+
project.create_release(Version.new(version))
|
151
|
+
mesg project.message
|
152
|
+
elsif project.prerelease?
|
153
|
+
arg.nil? or raise Error, "Illegal number of arguments"
|
154
|
+
project.create_release_from_prerelease
|
155
|
+
mesg project.message
|
156
|
+
else
|
157
|
+
raise Error, "You need to be on a release or pre-release branch to create a new release"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def cancel_release(arg)
|
162
|
+
project.cancel_release(Version.new(arg))
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_prerelease(arg)
|
166
|
+
check_clean
|
167
|
+
if project.release?
|
168
|
+
version = %w(major minor patch).include?(arg) ? project.version.increment(arg.to_sym) : Version.new(arg)
|
169
|
+
project.prepare_release(commit: false)
|
170
|
+
prerelease = project.create_prerelease(version)
|
171
|
+
mesg "Created pre-release #{prerelease.version}"
|
172
|
+
elsif project.prerelease?
|
173
|
+
arg.nil? or raise Error, "Illegal number of arguments"
|
174
|
+
prerelease = project.increment_prerelease
|
175
|
+
mesg "Created pre-release #{prerelease.prerelease_version}"
|
176
|
+
else
|
177
|
+
raise Error, "You need to be on a release branch to create a pre-release"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def prepare_migration(arg)
|
182
|
+
check_clean
|
183
|
+
version = Version.new(arg)
|
184
|
+
project.release? or raise "You need to be on a release or migration branch to prepare a migration"
|
185
|
+
project.prepare_migration(version)
|
186
|
+
mesg project.message
|
187
|
+
end
|
188
|
+
|
189
|
+
def create_feature(name)
|
190
|
+
check_clean
|
191
|
+
project.release? or raise "You ned to be on a release branch to create a feature"
|
192
|
+
project.create_feature(name)
|
193
|
+
mesg "Created feature '#{name}'"
|
194
|
+
end
|
195
|
+
|
196
|
+
def include_feature(name_or_version)
|
197
|
+
check_clean
|
198
|
+
project.prerelease? or raise Error, "You need to be on a pre-release branch to include a feature"
|
199
|
+
version = Version.try(name_or_version) ||
|
200
|
+
Version.new(project.branch.base_version, feature: name_or_version)
|
201
|
+
Git.branch?(version.to_s) or raise Error, "Can't find feature #{version}"
|
202
|
+
project.include_feature(version)
|
203
|
+
mesg "Included feature '#{name_or_version}'"
|
204
|
+
mesg "Please resolve eventual conflicts and then commit"
|
205
|
+
end
|
206
|
+
|
207
|
+
def upgrade
|
208
|
+
# TODO: Shutdown connections
|
209
|
+
project.database.version != project.version or raise Error, "Database already up to date"
|
210
|
+
project.backup
|
211
|
+
begin
|
212
|
+
project.upgrade
|
213
|
+
rescue RuntimeError
|
214
|
+
project.restore
|
215
|
+
raise Fail, "Failed upgrading database, rolled back to last version"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def backup(file = nil) project.backup(file) end
|
220
|
+
|
221
|
+
def restore(file = nil)
|
222
|
+
file.nil? || File.exist?(file) or raise Error, "Can't find #{file}"
|
223
|
+
project.restore(file)
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
def compute_version(version, arg)
|
228
|
+
if arg.nil?
|
229
|
+
nil
|
230
|
+
elsif %w(major minor patch).include?(arg)
|
231
|
+
version.increment(arg.to_sym)
|
232
|
+
else
|
233
|
+
Prick::Version.new(arg)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
data/lib/prick/project.rb
CHANGED
@@ -1,442 +1,350 @@
|
|
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.
|
17
|
-
# of the current directory
|
7
|
+
# Name of project. Persisted in the project state file
|
18
8
|
attr_reader :name
|
19
9
|
|
20
|
-
# Name of Postgresql user that
|
10
|
+
# Name of Postgresql user that owns the databases. Defaults to #name.
|
11
|
+
# Persisted in the project state file. TODO: Make writable
|
21
12
|
attr_reader :user
|
13
|
+
|
14
|
+
# Current branch
|
15
|
+
attr :branch
|
22
16
|
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
# The current release, prerelease or feature
|
27
|
-
def release() @builds_by_name[Git.current_branch] end
|
28
|
-
|
29
|
-
# The project database. The project database has the same name as the project
|
30
|
-
# and doesn't include a version. It does not have to exist
|
31
|
-
attr_reader :database
|
17
|
+
# Version of the current branch. If is defined as #branch.version
|
18
|
+
def version() branch.version end
|
32
19
|
|
33
|
-
#
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
# after regular releases and alphabetical between themselves
|
38
|
-
attr_reader :releases
|
39
|
-
|
40
|
-
# List of pre-releases
|
41
|
-
attr_reader :prereleases
|
20
|
+
# Project (default) database
|
21
|
+
def database(version = nil)
|
22
|
+
version ? Database.new("#{name}-#{version.truncate(:pre)}", user) : @database
|
23
|
+
end
|
42
24
|
|
43
|
-
#
|
44
|
-
attr_reader :
|
25
|
+
# Last commit message. TODO: Move to git.rb
|
26
|
+
attr_reader :message
|
27
|
+
|
28
|
+
# True if we're on a tag
|
29
|
+
def tag?() Git.detached? end
|
30
|
+
def self.tag?() Git.detached? end
|
31
|
+
|
32
|
+
# Classifiers
|
33
|
+
forward_methods :release?, :prerelease?, :feature?, :migration?, :@branch
|
34
|
+
|
35
|
+
def initialize(name, user, branch)
|
36
|
+
@name = name
|
37
|
+
@user = user
|
38
|
+
@branch = branch
|
39
|
+
@database = Database.new(name, user)
|
40
|
+
end
|
45
41
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
def self.load
|
43
|
+
name = Git.current_branch || Git.current_tag
|
44
|
+
if name =~ MIGRATION_RE
|
45
|
+
branch = MigrationRelease.load(name)
|
46
|
+
else
|
47
|
+
begin
|
48
|
+
version = Version.new(name)
|
49
|
+
rescue Version::FormatError
|
50
|
+
raise Fail, "Illegal branch name: #{name}"
|
51
|
+
end
|
52
|
+
if version.release?
|
53
|
+
branch = Release.load(name)
|
54
|
+
elsif version.pre?
|
55
|
+
branch = PreRelease.load(name, version)
|
56
|
+
elsif version.feature?
|
57
|
+
branch = Feature.load(name)
|
58
|
+
else
|
59
|
+
raise Oops
|
60
|
+
end
|
61
|
+
end
|
62
|
+
state = ProjectState.new.read
|
63
|
+
Project.new(state.name, state.user, branch)
|
64
|
+
end
|
52
65
|
|
53
|
-
|
54
|
-
|
66
|
+
def self.initialized?(directory)
|
67
|
+
File.directory?(directory) && Dir.chdir(directory) { ProjectState.new.exist? }
|
68
|
+
end
|
55
69
|
|
56
|
-
|
57
|
-
|
70
|
+
def self.initialize_directory(name, user, directory)
|
71
|
+
FileUtils.mkdir_p(directory)
|
72
|
+
Dir.chdir(directory) {
|
73
|
+
# Initialize git instance
|
74
|
+
Git.init
|
58
75
|
|
59
|
-
|
60
|
-
|
76
|
+
# Create prick version file
|
77
|
+
PrickVersion.new.write(VERSION)
|
61
78
|
|
62
|
-
|
63
|
-
|
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
|
72
|
-
end
|
79
|
+
# Create project state file
|
80
|
+
ProjectState.new(name: name, user: user).write
|
73
81
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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)
|
82
|
+
# Directories
|
83
|
+
DIRS.each { |dir|
|
84
|
+
!File.exist?(dir) or raise Fail, "Already initialized: Directory '#{dir}' exists"
|
85
|
+
}
|
86
|
+
FileUtils.mkdir_p(DIRS)
|
87
|
+
DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
|
86
88
|
|
87
|
-
|
88
|
-
|
89
|
+
# Copy default gitignore and schema files
|
90
|
+
Share.cp("gitignore", ".gitignore")
|
91
|
+
Share.cp("schemas", ".")
|
89
92
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
+
# Add everything so far
|
94
|
+
Git.add(".")
|
95
|
+
Git.commit("Initial import")
|
93
96
|
|
94
|
-
|
95
|
-
|
97
|
+
# Create initial release
|
98
|
+
release = Release.new(Version.zero, nil)
|
99
|
+
release.create
|
96
100
|
|
97
|
-
|
98
|
-
|
101
|
+
schema = Schema.new(SCHEMAS_DIR)
|
102
|
+
schema.version = release.version
|
103
|
+
Git.add schema.version_file
|
99
104
|
|
100
|
-
|
105
|
+
Git.commit "Release 0.0.0"
|
106
|
+
release.tag!
|
101
107
|
|
102
|
-
|
108
|
+
# Kill master branch
|
109
|
+
Git.delete_branch("master")
|
110
|
+
}
|
103
111
|
end
|
104
112
|
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
113
|
+
def build(database: self.database, version: nil)
|
114
|
+
clean(database)
|
115
|
+
if version
|
116
|
+
FileUtils.mkdir_p(TMP_DIR)
|
117
|
+
Dir.mktmpdir("clone-", TMP_DIR) { |dir|
|
118
|
+
Command.command "git clone . #{dir}"
|
119
|
+
Dir.chdir(dir) {
|
120
|
+
Git.checkout_tag(version)
|
121
|
+
project = Project.load
|
122
|
+
project.branch.build(database)
|
123
|
+
}
|
124
|
+
}
|
111
125
|
else
|
112
|
-
|
126
|
+
branch.build(database)
|
113
127
|
end
|
128
|
+
self
|
114
129
|
end
|
115
130
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
version = Version.new(name)
|
121
|
-
when Version
|
122
|
-
version = name_or_version
|
123
|
-
name = version.to_s
|
124
|
-
else
|
125
|
-
raise Internal, "Expected String or Version index, got #{name_or_version.class}"
|
126
|
-
end
|
127
|
-
@builds_by_name[name] = @builds_by_version[version] = branch
|
131
|
+
def load(file, database: self.database)
|
132
|
+
clean(database)
|
133
|
+
database.load(file)
|
134
|
+
self
|
128
135
|
end
|
129
136
|
|
130
|
-
def
|
131
|
-
|
132
|
-
|
133
|
-
name = version = name_or_version
|
134
|
-
case name_or_version
|
135
|
-
when String; @builds_by_name.key?(name)
|
136
|
-
when Version; @builds_by_version.key?(version)
|
137
|
-
when NilClass; nil
|
138
|
-
else
|
139
|
-
raise Internal, "Expected String or Version index, got #{name_or_version.class}"
|
140
|
-
end
|
137
|
+
def save(file, database: self.database)
|
138
|
+
database.save(file)
|
139
|
+
self
|
141
140
|
end
|
142
141
|
|
143
|
-
|
144
|
-
def self.initialize_directory(directory)
|
145
|
-
FileUtils.mkdir_p(directory)
|
146
|
-
Dir.chdir(directory) {
|
147
|
-
DIRS.each { |dir|
|
148
|
-
!File.exist?(dir) or raise Fail, "Already initialized: Directory '#{dir}' exists"
|
149
|
-
}
|
142
|
+
def cache_file(version) File.join(CACHE_DIR, "#{database(version)}.sql.gz") end
|
150
143
|
|
151
|
-
|
152
|
-
|
153
|
-
|
144
|
+
def make(database, subject)
|
145
|
+
Schema.built? or raise Error, "Schema is not built into project database"
|
146
|
+
Schema.make(database, subject)
|
154
147
|
end
|
155
148
|
|
156
|
-
|
157
|
-
|
158
|
-
def self.initialize_project(name = File.basename(Dir.getwd), user = name || ENV['USER'])
|
159
|
-
FileUtils.cp("#{SHARE_PATH}/gitignore", ".gitignore")
|
160
|
-
FileUtils.cp("#{SHARE_PATH}/schemas/prick/schema.sql", "schemas/prick")
|
161
|
-
FileUtils.cp("#{SHARE_PATH}/schemas/prick/data.sql", "schemas/prick")
|
162
|
-
Git.init
|
163
|
-
Git.add(".")
|
164
|
-
Git.commit("Initial import")
|
165
|
-
project = Project.new(name, user)
|
166
|
-
release = Release.new(project, nil, Version.new("0.0.0"))
|
167
|
-
release.create
|
168
|
-
|
169
|
-
Git.delete_branch("master")
|
149
|
+
def backup(path = nil)
|
150
|
+
save path || File.join(SPOOL_DIR, "#{name}-#{Time.now.utc.strftime("%Y%m%d-%H%M%S")}.sql.gz")
|
170
151
|
end
|
171
152
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
### COMMON METHODS
|
176
|
-
|
177
|
-
def checkout(name)
|
178
|
-
check_clean
|
179
|
-
Git.checkout_branch(name)
|
153
|
+
def restore(path = nil)
|
154
|
+
load path || Dir.glob(File.join(SPOOL_DIR, "#{name}-*.sql.gz")).sort.last
|
180
155
|
end
|
181
156
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
history = release.history.keys.reverse.drop(1)
|
187
|
-
history.select! { |release| release.version >= database.version }
|
188
|
-
history.each { |release| release.migration.migrate(database.name) }
|
189
|
-
database.version = release.version
|
157
|
+
def prepare_release(commit: true)
|
158
|
+
release = ReleaseMigration.new(nil, branch.version).create
|
159
|
+
submit "Prepared new release based on #{version}", commit
|
160
|
+
release
|
190
161
|
end
|
191
162
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
Git.commit "Created feature #{name}"
|
163
|
+
def prepare_schema(name, commit: true)
|
164
|
+
path = File.join(SCHEMAS_DIR, name)
|
165
|
+
FileUtils.mkdir_p(path)
|
166
|
+
Git.add Share.cp("schema/*", path, clobber: false, templates: { 'SCHEMA' => name })
|
167
|
+
File.open(branch.schema.yml_file, "a") { |f| f.write("- #{name}\n") }
|
168
|
+
Git.add(branch.schema.yml_file)
|
169
|
+
submit "Added schema #{name}", commit
|
200
170
|
end
|
201
171
|
|
202
|
-
def
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
172
|
+
def prepare_diff(from_version)
|
173
|
+
to_database = nil
|
174
|
+
begin
|
175
|
+
from_name = "#{name}-base"
|
176
|
+
from_database = Database.new(from_name, user)
|
177
|
+
build(database: from_database, version: from_version)
|
208
178
|
|
209
|
-
|
179
|
+
to_name = "#{name}-next"
|
180
|
+
to_database = Database.new(to_name, user)
|
181
|
+
build(database: to_database)
|
210
182
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
# workflow. Requires a :prepared state
|
215
|
-
def prepare_release
|
216
|
-
check_clean
|
217
|
-
release? or raise Error, "Need to be on a release branch to prepare a new release"
|
218
|
-
release.prepare
|
219
|
-
end
|
183
|
+
# Helpful double-check
|
184
|
+
Diff.same?(to_database.name, database.name) or
|
185
|
+
raise Error, "Schema and project database are not synchronized"
|
220
186
|
|
221
|
-
|
222
|
-
|
223
|
-
|
187
|
+
if release?
|
188
|
+
dir = branch.migration.migration_dir
|
189
|
+
else
|
190
|
+
dir = branch.migration.features_dir
|
191
|
+
end
|
224
192
|
|
225
|
-
|
226
|
-
|
227
|
-
build # if !database.loaded?
|
193
|
+
# dir = branch.migration.features_dir || branch.migration.migration_dir
|
194
|
+
to_file = File.join(dir, DIFF_FILE)
|
228
195
|
|
229
|
-
|
230
|
-
|
231
|
-
|
196
|
+
if prerelease?
|
197
|
+
branch.migrate_features(from_database)
|
198
|
+
end
|
232
199
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
241
|
-
else
|
242
|
-
raise Error, "Need to be on a release or pre-prelease branch to create a new release"
|
200
|
+
Diff.new(from_name, to_name).write(to_file)
|
201
|
+
ensure
|
202
|
+
from_database&.drop
|
203
|
+
to_database&.drop
|
243
204
|
end
|
244
|
-
new_release.create
|
245
|
-
new_release
|
246
205
|
end
|
247
206
|
|
248
|
-
def
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
207
|
+
def prepare_migration(from, commit: true)
|
208
|
+
to = branch.version
|
209
|
+
migration_release = MigrationRelease.new(to, from)
|
210
|
+
migration_release.create
|
211
|
+
submit "Prepared migration from #{from} to #{to}", commit
|
212
|
+
migration_release
|
253
213
|
end
|
254
214
|
|
255
|
-
def
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
215
|
+
def create_release(version, commit: true)
|
216
|
+
check_migration(branch.version) or raise Error, "Schema/migration mismatch"
|
217
|
+
release = Release.new(version, branch.version)
|
218
|
+
release.create
|
219
|
+
submit "Created release #{version}", commit
|
220
|
+
release.tag!
|
221
|
+
build
|
222
|
+
release
|
260
223
|
end
|
261
224
|
|
262
|
-
def
|
263
|
-
|
264
|
-
release? or raise Error, "Need to be on a release branch to create a feature"
|
265
|
-
feature = Feature.new(self, release, name)
|
266
|
-
feature.create
|
225
|
+
def cancel_release(version, commit: true)
|
226
|
+
Git.cancel_tag(version)
|
267
227
|
end
|
268
228
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
release.
|
229
|
+
def create_release_from_prerelease(commit: true)
|
230
|
+
check_migration(branch.base_version) or raise Error, "Schema/migration mismatch"
|
231
|
+
release = Release.new(branch.version.truncate(:pre), branch.base_version)
|
232
|
+
release.create
|
233
|
+
submit "Released #{release.version}", commit
|
234
|
+
release.tag!
|
235
|
+
build
|
236
|
+
release
|
275
237
|
end
|
276
238
|
|
277
|
-
def
|
278
|
-
|
279
|
-
|
280
|
-
|
239
|
+
def create_prerelease(version, commit: true)
|
240
|
+
prerelease = PreRelease.new(version.increment(:pre), branch.version)
|
241
|
+
prerelease.create
|
242
|
+
submit "Created pre-release #{prerelease.version}", commit
|
243
|
+
build
|
244
|
+
prerelease
|
281
245
|
end
|
282
246
|
|
283
|
-
def
|
247
|
+
def increment_prerelease(commit: true)
|
248
|
+
prerelease = branch.increment
|
249
|
+
prerelease.create
|
250
|
+
submit "Created pre-release #{prerelease.version}", commit
|
251
|
+
build
|
252
|
+
prerelease
|
284
253
|
end
|
285
254
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
release = self[version] or raise Error, "Can't find release #{version}"
|
293
|
-
release.database.recreate
|
294
|
-
FileUtils.mkdir_p(CACHE_DIR)
|
295
|
-
if !File.directory?(File.join(CLONE_DIR, ".git"))
|
296
|
-
FileUtils.rm_rf(CLONE_DIR)
|
297
|
-
Command.command "git clone . #{CLONE_DIR}"
|
298
|
-
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
|
-
end
|
307
|
-
end
|
255
|
+
# # Turned out to be a bad idea
|
256
|
+
# def retarget_migration(version, commit: true)
|
257
|
+
# branch.retarget(version)
|
258
|
+
# commit "Retargeted to #{version}" if commit
|
259
|
+
# branch
|
260
|
+
# end
|
308
261
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
database.recreate
|
314
|
-
database.load(file)
|
262
|
+
def create_feature(name, commit: true)
|
263
|
+
feature = Feature.new(name, version)
|
264
|
+
feature.create
|
265
|
+
submit "Created feature #{name}", commit
|
315
266
|
end
|
316
267
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
def load_database(version, file = nil)
|
321
|
-
check_clean
|
322
|
-
!version.nil? || !file.nil? or raise Fail, "Not both version and file can be nil"
|
323
|
-
if version
|
324
|
-
release = self[version]
|
325
|
-
file ||= release.archive.path
|
326
|
-
File.file?(file) or raise Error, "Can't find #{file}"
|
327
|
-
release.database.recreate
|
328
|
-
release.database.load(file)
|
329
|
-
else
|
330
|
-
database.recreate
|
331
|
-
database.load(file)
|
332
|
-
end
|
268
|
+
def include_feature(feature_version, commit: false)
|
269
|
+
Git.merge_branch(feature_version, exclude_files: [branch.schema.version_file], fail: true)
|
270
|
+
branch.include(feature_version)
|
333
271
|
end
|
334
272
|
|
335
|
-
#
|
336
|
-
#
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
#
|
341
|
-
|
342
|
-
|
343
|
-
#
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
databases(all: true, project: project).each(&:drop)
|
273
|
+
# Checks that the migration of the current branch takes the database from
|
274
|
+
# the base version to the content of the schema
|
275
|
+
def check_migration(from_version)
|
276
|
+
begin
|
277
|
+
from_db = Database.new("#{name}-base", user)
|
278
|
+
to_db = Database.new("#{name}-next", user)
|
279
|
+
build(database: from_db, version: from_version)
|
280
|
+
build(database: to_db)
|
281
|
+
migration = # FIXME: This may be a repeated pattern
|
282
|
+
if migration?
|
283
|
+
branch.migration
|
284
|
+
elsif prerelease?
|
285
|
+
branch.migration
|
286
|
+
else
|
287
|
+
Migration.new(nil, branch.directory)
|
288
|
+
end
|
289
|
+
migration.migrate(from_db)
|
290
|
+
Diff.new(from_db, to_db).same?
|
291
|
+
ensure
|
292
|
+
from_db&.drop
|
293
|
+
to_db&.drop
|
357
294
|
end
|
358
295
|
end
|
359
296
|
|
360
|
-
|
361
|
-
def
|
362
|
-
|
297
|
+
# TODO: Make production-only and add a to-argument
|
298
|
+
def upgrade
|
299
|
+
branches = list_upgrades(database.version, branch.version)
|
300
|
+
FileUtils.mkdir_p(TMP_DIR)
|
301
|
+
Dir.mktmpdir("clone-", TMP_DIR) { |dir|
|
302
|
+
Command.command "git clone . #{dir}"
|
303
|
+
Dir.chdir(dir) {
|
304
|
+
branches.each { |branch|
|
305
|
+
branch.checkout_release
|
306
|
+
project = Project.load
|
307
|
+
project.branch.migrate(project.database)
|
308
|
+
}
|
309
|
+
}
|
310
|
+
}
|
363
311
|
end
|
364
312
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
313
|
+
def list_releases(all: false)
|
314
|
+
Git.list_tags(include_cancelled: all).grep(RELEASE_RE) { |tag|
|
315
|
+
version = $2
|
316
|
+
dir = File.join(RELEASES_DIR, version)
|
317
|
+
migration_state = MigrationState.new(dir).read(tag: tag)
|
318
|
+
Release.new(migration_state.version, migration_state.base_version)
|
369
319
|
}
|
370
320
|
end
|
371
321
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
Git.list_branches.each { |branch|
|
380
|
-
if Version.version?(branch)
|
381
|
-
version = Version.new(branch)
|
382
|
-
if version.feature?
|
383
|
-
features[version] = version.truncate(:feature)
|
384
|
-
elsif version.pre?
|
385
|
-
prereleases[version] = nil
|
386
|
-
elsif version.release?
|
387
|
-
releases[version] = nil
|
388
|
-
else
|
389
|
-
raise Internal, "Unexpected state for #{version}"
|
390
|
-
end
|
391
|
-
else
|
392
|
-
@orphan_git_branches << branch
|
393
|
-
end
|
322
|
+
# TODO
|
323
|
+
# list_migrations(all: false)
|
324
|
+
def list_migrations
|
325
|
+
Git.list_branches.grep(MIGRATION_RE) { |branch|
|
326
|
+
from_version = Version.new($1)
|
327
|
+
to_version = Version.new($4)
|
328
|
+
MigrationRelease.new(to_version, from_version)
|
394
329
|
}
|
330
|
+
end
|
395
331
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
version = Version.new(node)
|
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
|
412
|
-
end
|
413
|
-
}
|
332
|
+
def list_upgrades(from, to)
|
333
|
+
edges = (list_releases + list_migrations).map { |branch| [branch.base_version, branch.version, branch] }
|
334
|
+
(Algorithm.shortest_path(edges, from, to) || []).map(&:last)
|
335
|
+
end
|
414
336
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
@
|
337
|
+
private
|
338
|
+
def submit(msg, commit = true)
|
339
|
+
Git.commit msg if commit
|
340
|
+
@message = msg
|
341
|
+
end
|
419
342
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
end
|
426
|
-
}
|
427
|
-
@prereleases.sort!
|
428
|
-
|
429
|
-
if !Git.detached?
|
430
|
-
features.each { |feature, base_release|
|
431
|
-
# if File.exist?(base_release_file)
|
432
|
-
# recursive include of dependent releases
|
433
|
-
|
434
|
-
if base_release
|
435
|
-
(@features[base_release] ||= []) << Feature.new(self, self[base_release], feature.feature)
|
436
|
-
else
|
437
|
-
@ignored_release_nodes << release
|
438
|
-
end
|
439
|
-
}
|
343
|
+
def clean(database)
|
344
|
+
if database.exist?
|
345
|
+
database.recreate if database.loaded?
|
346
|
+
else
|
347
|
+
database.create
|
440
348
|
end
|
441
349
|
end
|
442
350
|
end
|