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
data/lib/prick/command.rb
CHANGED
@@ -14,14 +14,13 @@ module Command
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
# Execute the shell command 'cmd' and return
|
18
|
-
# strings
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# :fail is true. The exit status of the last command is stored in ::status
|
17
|
+
# Execute the shell command 'cmd' and return standard output as an array of
|
18
|
+
# strings while stderr is passed through unless stderr: is false. If stderr:
|
19
|
+
# is true, it returns a tuple of [stdout, stderr] instead. The shell command
|
20
|
+
# is executed with the `errexit` and `pipefail` bash options
|
21
|
+
#
|
22
|
+
# It raises a Command::Error exception if the command fails unless :fail is
|
23
|
+
# true. The exit status of the last command is stored in ::status
|
25
24
|
#
|
26
25
|
def command(cmd, stderr: false, fail: true)
|
27
26
|
cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
|
@@ -62,10 +61,19 @@ module Command
|
|
62
61
|
pr[0].close
|
63
62
|
pe[0].close
|
64
63
|
|
64
|
+
result =
|
65
|
+
case stderr
|
66
|
+
when true; [out, err]
|
67
|
+
when false; out
|
68
|
+
when NilClass; $stderr.puts err
|
69
|
+
else
|
70
|
+
raise Internal, "Unexpected value for :stderr - #{stderr.inspect}"
|
71
|
+
end
|
72
|
+
|
65
73
|
if @status == 0 || fail == false
|
66
|
-
|
67
|
-
|
68
|
-
raise Command::Error.new((out + err).join("\n") + "\n", status, out, err)
|
74
|
+
result
|
75
|
+
elsif fail
|
76
|
+
raise Command::Error.new("\n" + cmd + "\n" + (out + err).join("\n") + "\n", status, out, err)
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
data/lib/prick/constants.rb
CHANGED
@@ -1,28 +1,50 @@
|
|
1
1
|
|
2
2
|
module Prick
|
3
|
+
### DIRECTORIES AND FILE NAMES
|
4
|
+
|
5
|
+
# Path to the .prick-version file
|
6
|
+
PRICK_VERSION_FILE = ".prick-version"
|
7
|
+
|
8
|
+
# State files
|
9
|
+
PROJECT_STATE_FILE = ".prick-project"
|
10
|
+
FEATURES_STATE_FILE = ".prick-features"
|
11
|
+
MIGRATION_STATE_FILE = ".prick-migration"
|
12
|
+
MIGRATIONS_FILE = "migrations.sql"
|
13
|
+
|
14
|
+
# Diff file
|
15
|
+
DIFF_FILE = "diff.sql"
|
16
|
+
|
3
17
|
# Shared files (part of the installation)
|
4
18
|
SHARE_PATH = "#{File.dirname(File.dirname(__dir__))}/share"
|
19
|
+
LIBEXEC_PATH = "#{File.dirname(File.dirname(__dir__))}/libexec"
|
20
|
+
|
21
|
+
STRIP_COMMENTS_COMMAND = File.join(LIBEXEC_PATH, "strip-comments")
|
5
22
|
|
6
23
|
# Project directories
|
7
24
|
DIRS = [
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
PUBLIC_DIR = "#{SCHEMA_DIR}/public",
|
25
|
+
MIGRATIONS_DIR = "migrations",
|
26
|
+
RELEASES_DIR = "releases",
|
27
|
+
SCHEMAS_DIR = "schemas",
|
28
|
+
PRICK_DIR = "#{SCHEMAS_DIR}/prick",
|
29
|
+
PUBLIC_DIR = "#{SCHEMAS_DIR}/public",
|
14
30
|
VAR_DIR = "var",
|
15
31
|
CACHE_DIR = "#{VAR_DIR}/cache",
|
32
|
+
SPOOL_DIR = "#{VAR_DIR}/spool",
|
16
33
|
TMP_DIR = "tmp",
|
17
34
|
CLONE_DIR = "tmp/clone",
|
18
35
|
SPEC_DIR = "spec"
|
19
36
|
]
|
20
37
|
|
38
|
+
# Path to prick data file
|
39
|
+
# PRICK_DATA_PATH = File.join(SCHEMAS_DIR, "prick", "data.sql")
|
40
|
+
SCHEMA_VERSION_PATH = File.join(SCHEMAS_DIR, "prick", "data.sql")
|
41
|
+
|
21
42
|
# Dump files
|
22
43
|
DUMP_EXT = "dump.gz"
|
23
44
|
DUMP_GLOB = "*-[0-9]*.#{DUMP_EXT}"
|
24
45
|
def self.dump_glob(project_name) "#{project_name}-*.#{DUMP_EXT}" end
|
25
46
|
|
47
|
+
### REGULAR EXPRESSIONS
|
26
48
|
|
27
49
|
# Matches an identifier. Identifiers consist of lower case letters, digits
|
28
50
|
# and underscores but not dashes because they're used as separators
|
@@ -56,7 +78,7 @@ module Prick
|
|
56
78
|
USER_NAME_SUB_RE = NAME_SUB_RE
|
57
79
|
USER_NAME_RE = NAME_RE
|
58
80
|
|
59
|
-
# Matches a major.minor.patch version
|
81
|
+
# Matches a major.minor.patch ('MMP') version
|
60
82
|
#
|
61
83
|
# The *_SEMVER REs are derived from the canonical RE
|
62
84
|
#
|
@@ -119,19 +141,6 @@ module Prick
|
|
119
141
|
/x
|
120
142
|
RELEASE_RE = /^#{RELEASE_SUB_RE}$/
|
121
143
|
|
122
|
-
# Migration RE. The syntax is <version>_<version>
|
123
|
-
#
|
124
|
-
# The RE defines the following captures
|
125
|
-
# $1 - from version
|
126
|
-
# $2 - from custom name, can be nil
|
127
|
-
# $3 - from semantic version
|
128
|
-
# $4 - to version
|
129
|
-
# $5 - to custom name, can be nil
|
130
|
-
# $6 - to semantic version
|
131
|
-
#
|
132
|
-
MIGRATION_SUB_RE = /(#{RELEASE_SUB_RE})_(#{RELEASE_SUB_RE})/
|
133
|
-
MIGRATION_RE = /^#{MIGRATION_SUB_RE}$/
|
134
|
-
|
135
144
|
# Matches a prerelease branch
|
136
145
|
#
|
137
146
|
# The RE defines the following captures:
|
@@ -154,6 +163,19 @@ module Prick
|
|
154
163
|
FEATURE_SUB_RE = /#{ABSTRACT_RELEASE_SUB_RE}_(#{FEATURE_NAME_SUB_RE})/
|
155
164
|
FEATURE_RE = /^#{FEATURE_SUB_RE}$/
|
156
165
|
|
166
|
+
# Migration RE. The syntax is <version>_<version>
|
167
|
+
#
|
168
|
+
# The RE defines the following captures
|
169
|
+
# $1 - from version
|
170
|
+
# $2 - from custom name, can be nil
|
171
|
+
# $3 - from semantic version
|
172
|
+
# $4 - to version
|
173
|
+
# $5 - to custom name, can be nil
|
174
|
+
# $6 - to semantic version
|
175
|
+
#
|
176
|
+
MIGRATION_SUB_RE = /(#{RELEASE_SUB_RE})_(#{RELEASE_SUB_RE})/
|
177
|
+
MIGRATION_RE = /^#{MIGRATION_SUB_RE}$/
|
178
|
+
|
157
179
|
# Project release RE. The general syntax is '<project>-<custom>-<version>'
|
158
180
|
#
|
159
181
|
# The RE defines the following captures:
|
data/lib/prick/database.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
require "prick/ensure.rb"
|
2
|
+
#require "prick/ensure.rb"
|
3
3
|
|
4
4
|
module Prick
|
5
5
|
class Database
|
@@ -42,12 +42,14 @@ module Prick
|
|
42
42
|
def recreate() drop if exist?; create; @version = nil end
|
43
43
|
def drop() Rdbms.drop_database(name, fail: false); @version = nil end
|
44
44
|
|
45
|
-
def loaded?()
|
45
|
+
def loaded?() !version.nil? end
|
46
46
|
def load(file) Rdbms.load(name, file, user: user); @version = nil end
|
47
47
|
|
48
48
|
def save(file) Rdbms.save(name, file); @version = nil end
|
49
49
|
|
50
|
-
|
50
|
+
def to_s() name end
|
51
|
+
|
52
|
+
# include Ensure
|
51
53
|
|
52
54
|
private
|
53
55
|
@states = {
|
data/lib/prick/diff.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Prick
|
2
|
+
class Diff
|
3
|
+
def initialize(db1, db2)
|
4
|
+
@db1, @db2 = db1, db2
|
5
|
+
@diffed = false
|
6
|
+
@diff = nil
|
7
|
+
@result = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.same?(db1, db2) Diff.new(db1, db2).same? end
|
11
|
+
|
12
|
+
# Return true if the two databases are equal. Named #same? to avoid name
|
13
|
+
# collision with the built in #equal?
|
14
|
+
def same?
|
15
|
+
do_diff if @result.nil?
|
16
|
+
@result
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return the diff between the two databases or nil if they're equal
|
20
|
+
def read
|
21
|
+
@diffed ? @diff : do_diff
|
22
|
+
end
|
23
|
+
|
24
|
+
# Write the diff between the databases to the given file. Return true if
|
25
|
+
# the databases are equal
|
26
|
+
def write(file)
|
27
|
+
if @diffed
|
28
|
+
IO.write(file, @diff)
|
29
|
+
else
|
30
|
+
do_diff(file)
|
31
|
+
end
|
32
|
+
@result
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def do_diff(file = nil)
|
37
|
+
file_arg = file ? ">#{file}" : ""
|
38
|
+
command = "migra --unsafe postgres:///#{@db1} postgres:///#{@db2} #{file_arg}"
|
39
|
+
stdout = Command.command command, fail: false
|
40
|
+
[0,2].include? Command.status or
|
41
|
+
raise Fail, "migra command failed with status #{Command.status}: #{command}"
|
42
|
+
@result = Command.status == 0
|
43
|
+
@diffed = !file
|
44
|
+
@diff = @diffed && !@result ? stdout : nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/prick/exceptions.rb
CHANGED
@@ -2,12 +2,24 @@
|
|
2
2
|
module Prick
|
3
3
|
class Error < RuntimeError; end
|
4
4
|
class Fail < Error; end
|
5
|
+
|
5
6
|
class NotYet < NotImplementedError
|
6
|
-
def initialize() super("
|
7
|
+
def initialize() super("Internal error: Not yet implemented") end
|
8
|
+
end
|
9
|
+
|
10
|
+
class NotThis < ScriptError
|
11
|
+
def initialize() super("Internal error: Abstract method called") end
|
7
12
|
end
|
8
|
-
|
9
|
-
|
13
|
+
|
14
|
+
class Abstract < ScriptError
|
15
|
+
def initialize() super("Internal error: Abstract method called") end
|
10
16
|
end
|
17
|
+
|
18
|
+
AbstractMethod = Abstract
|
19
|
+
|
11
20
|
class Internal < ScriptError; end
|
21
|
+
class Oops < Internal
|
22
|
+
def initialize() super("Oops, this shouldn't happen") end
|
23
|
+
end
|
12
24
|
end
|
13
25
|
|
data/lib/prick/git.rb
CHANGED
@@ -9,34 +9,41 @@ module Prick
|
|
9
9
|
Command.command "git init"
|
10
10
|
end
|
11
11
|
|
12
|
-
# Returns true if the repository has no modified files
|
13
|
-
# repository to have at least one commit. Creating
|
14
|
-
# Project::initialize_directory guarantees that
|
12
|
+
# Returns true if the repository has no modified files or unresolved
|
13
|
+
# conflicts. Requires the repository to have at least one commit. Creating
|
14
|
+
# the repository using Project::initialize_directory guarantees that
|
15
15
|
def self.clean?()
|
16
16
|
Command.command("git status").find { |l|
|
17
|
-
l =~
|
17
|
+
(l =~ /^Changes to be committed:/) ||
|
18
|
+
(l =~ /^Unmerged paths:/) ||
|
19
|
+
(l =~ /^Changes not staged for commit:/)
|
18
20
|
}.nil?
|
19
21
|
end
|
20
22
|
|
21
23
|
# Returns true if the repository is on a detached branch
|
22
24
|
def self.detached?
|
23
|
-
|
24
|
-
line =~ /^ref:/ ? false : true
|
25
|
+
Command.command? "git symbolic-ref -q HEAD", expect: 1
|
25
26
|
end
|
26
27
|
|
27
28
|
# The current tag. This is only defined if the repository is in "detached
|
28
29
|
# head" mode
|
29
|
-
def self.current_tag()
|
30
|
-
|
30
|
+
def self.current_tag()
|
31
|
+
self.detached? ? Command.command("git describe --tags").first.sub(/^v/, "") : nil
|
32
|
+
end
|
33
|
+
|
31
34
|
# Return true if `version` has an associated tag
|
32
35
|
def self.tag?(version)
|
33
|
-
!list_tags.grep(version).empty?
|
36
|
+
!list_tags.grep(version.to_s).empty?
|
34
37
|
end
|
35
38
|
|
36
|
-
# List tag versions
|
37
39
|
# Create version tag
|
38
|
-
def self.create_tag(version)
|
39
|
-
Command.command "git tag -a 'v#{version}' -m '
|
40
|
+
def self.create_tag(version, message: "Release #{version}", commit_id: nil)
|
41
|
+
Command.command "git tag -a 'v#{version}' -m '#{message}' #{commit_id}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a cancel-version tag
|
45
|
+
def self.cancel_tag(version)
|
46
|
+
create_tag("#{version}_cancelled", message: "Cancel #{version}", commit_id: tag_id(version))
|
40
47
|
end
|
41
48
|
|
42
49
|
def self.delete_tag(version, remote: false)
|
@@ -44,18 +51,30 @@ module Prick
|
|
44
51
|
Command.command("git push --delete origin 'v#{version}'", fail: false) if remote
|
45
52
|
end
|
46
53
|
|
54
|
+
def self.tag_id(version)
|
55
|
+
Command.command("git rev-parse 'v#{version}^{}'").first
|
56
|
+
end
|
57
|
+
|
47
58
|
# Checkout a version tag as a detached head
|
48
59
|
def self.checkout_tag(version)
|
49
60
|
Command.command "git checkout 'v#{version}'"
|
50
61
|
end
|
51
62
|
|
52
|
-
def self.list_tags
|
53
|
-
Command.command("git tag")
|
63
|
+
def self.list_tags(include_cancelled: false)
|
64
|
+
tags = Command.command("git tag")
|
65
|
+
if !include_cancelled
|
66
|
+
cancelled = tags.select { |tag| tag =~ /_cancelled$/ }
|
67
|
+
for cancel_tag in cancelled
|
68
|
+
tags.delete(cancel_tag)
|
69
|
+
tags.delete(cancel_tag.sub(/_cancelled$/, ""))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
tags.map { |tag| tag = tag[1..-1] }
|
54
73
|
end
|
55
74
|
|
56
|
-
# Name of the current branch
|
75
|
+
# Name of the current branch. This is nil if on a tag ("detached HEAD")
|
57
76
|
def self.current_branch()
|
58
|
-
Command.command("git rev-parse --abbrev-ref HEAD").first
|
77
|
+
self.detached? ? nil : Command.command("git rev-parse --abbrev-ref HEAD").first
|
59
78
|
end
|
60
79
|
|
61
80
|
# Check if branch exist
|
@@ -94,12 +113,18 @@ module Prick
|
|
94
113
|
|
95
114
|
Command.command "git merge --no-commit #{name}", fail: false
|
96
115
|
|
97
|
-
#
|
98
|
-
files.each { |path, content|
|
116
|
+
# Restore excluded files
|
117
|
+
files.each { |path, content|
|
99
118
|
File.open(path, "w") { |file| file.puts(content) }
|
100
119
|
# Resolve git unmerged status
|
101
120
|
Git.add(path)
|
102
121
|
}
|
122
|
+
|
123
|
+
# TODO Detect outstanding merges
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.merge_tag(name, exclude_files: [], fail: false)
|
127
|
+
merge_branch(name, exclude_files: exclude_files, fail: fail)
|
103
128
|
end
|
104
129
|
|
105
130
|
# List branches
|
@@ -109,7 +134,7 @@ module Prick
|
|
109
134
|
|
110
135
|
# Add a file to the index of the current branch
|
111
136
|
def self.add(*files)
|
112
|
-
Array(files).each { |file|
|
137
|
+
Array(files).flatten.each { |file|
|
113
138
|
Dir.chdir(File.dirname(file)) {
|
114
139
|
Command.command "git add '#{File.basename(file)}'"
|
115
140
|
}
|
@@ -127,7 +152,7 @@ module Prick
|
|
127
152
|
end.map { |l| "#{l}\n" }
|
128
153
|
end
|
129
154
|
|
130
|
-
# Return content of file
|
155
|
+
# Return content of file as a String
|
131
156
|
def self.read(file, tag: nil, branch: nil)
|
132
157
|
!(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
|
133
158
|
if tag
|
@@ -135,7 +160,7 @@ module Prick
|
|
135
160
|
else
|
136
161
|
branch ||= "HEAD"
|
137
162
|
Command.command "git show #{branch}:#{file}"
|
138
|
-
end.join("\n")
|
163
|
+
end.join("\n") + "\n"
|
139
164
|
end
|
140
165
|
|
141
166
|
def self.rm(file)
|
data/lib/prick/migration.rb
CHANGED
@@ -1,230 +1,210 @@
|
|
1
1
|
|
2
|
-
require 'yaml'
|
3
|
-
|
4
2
|
module Prick
|
3
|
+
# A Migration consists of a .prick-features file, a .prick-migration file, and a
|
4
|
+
# set of SQL migration files. Features can be include as subdirectories
|
5
|
+
#
|
5
6
|
class Migration
|
6
|
-
|
7
|
+
# Directory of the .prick_migration file. It is nil in CurrentRelease
|
8
|
+
# objects that supports the single-developer workflow
|
9
|
+
def migration_dir() @migration_state && File.dirname(@migration_state.path) end
|
7
10
|
|
8
|
-
|
11
|
+
# Directory of the .prick_features file. nil for the initial 0.0.0
|
12
|
+
# migration. Note that the feature and the migration directories can be the
|
13
|
+
# same. The features directory also contains the auto-generated files
|
14
|
+
# diff.sql, features.sql, features.yml, and the user-defined migrations.sql file
|
15
|
+
def features_dir() @features_state && File.dirname(@features_state.path) end
|
9
16
|
|
10
|
-
|
11
|
-
|
17
|
+
# Directory of the migration. Alias for #features_dir. It should be used in
|
18
|
+
# derived classes when the features and migration directory are the same
|
19
|
+
def dir() features_dir end
|
12
20
|
|
13
|
-
#
|
14
|
-
def
|
21
|
+
# Migration file
|
22
|
+
def migrations_file() File.join(dir, MIGRATIONS_FILE) end
|
15
23
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
24
|
+
# Version of the result of running this migration. `version` is nil for the
|
25
|
+
# current release and features that are not part of a pre-release. Needs to
|
26
|
+
# be redefined in subclasses where migration_dir is nil
|
27
|
+
forward_method :version, :version=, :@migration_state
|
20
28
|
|
21
|
-
|
22
|
-
|
23
|
-
|
29
|
+
# Base version of the migration. `base_version` is nil for the initial
|
30
|
+
# 0.0.0 migration. Needs to be redefined in subclasses where migration_dir
|
31
|
+
# is nil
|
32
|
+
forward_method :base_version, :base_version=, :@migration_state
|
24
33
|
|
25
|
-
|
26
|
-
|
34
|
+
# List of features in this migration. The features ordered in the same
|
35
|
+
# order as they're included
|
36
|
+
forward_method :features, :@features_state
|
27
37
|
|
28
|
-
|
38
|
+
# Note that migration_dir can be nil
|
39
|
+
def initialize(migration_dir, features_dir)
|
40
|
+
@migration_state = migration_dir && MigrationState.new(migration_dir)
|
41
|
+
@features_state = features_dir && FeaturesState.new(features_dir)
|
42
|
+
end
|
29
43
|
|
30
|
-
def
|
44
|
+
def dump
|
45
|
+
puts "#{self.class}"
|
46
|
+
indent {
|
47
|
+
puts "migration_dir: #{migration_dir}"
|
48
|
+
puts "features_dir: #{features_dir}"
|
49
|
+
puts "version: #{version}"
|
50
|
+
puts "base_version: #{base_version}"
|
51
|
+
}
|
52
|
+
end
|
31
53
|
|
32
|
-
|
33
|
-
|
54
|
+
# Needs to be redefined in ReleaseMigration. This default implementation
|
55
|
+
# assumes #migration_dir is equal to #features_dir
|
56
|
+
def self.load(directory) self.new(directory, directory).load end
|
34
57
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
File.join(FEATURE_DIR, version)
|
40
|
-
end
|
58
|
+
def load
|
59
|
+
@migration_state&.read
|
60
|
+
@features_state.read
|
61
|
+
self
|
41
62
|
end
|
42
63
|
|
43
|
-
def
|
44
|
-
|
64
|
+
def save
|
65
|
+
@migration_state&.write
|
66
|
+
@features_state.write
|
67
|
+
Git.add(@migration_state.path) if @migration_state
|
68
|
+
Git.add(@features_state.path)
|
69
|
+
self
|
45
70
|
end
|
46
71
|
|
47
|
-
|
48
|
-
|
72
|
+
# Return true if this is the initial 0.0.0 migration
|
73
|
+
def initial?() version&.zero? end
|
74
|
+
|
75
|
+
def exist?()
|
76
|
+
(@migration_state.nil? || @migration_state.exist?) && (initial? || @features_state.exist?)
|
49
77
|
end
|
50
78
|
|
51
|
-
def
|
52
|
-
|
79
|
+
def create(templates_pattern = nil)
|
80
|
+
if @migration_state && !@migration_state.exist?
|
81
|
+
FileUtils.mkdir_p(migration_dir) if migration_dir
|
82
|
+
@migration_state.create
|
83
|
+
Git.add(@migration_state.path)
|
84
|
+
end
|
85
|
+
if !initial? && !@features_state.exist?
|
86
|
+
FileUtils.mkdir_p(features_dir)
|
87
|
+
@features_state.create
|
88
|
+
Git.add(@features_state.path)
|
89
|
+
end
|
90
|
+
if !initial? && templates_pattern
|
91
|
+
files = Share.cp(templates_pattern, features_dir, clobber: false)
|
92
|
+
Git.add files
|
93
|
+
end
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def insert_feature(version)
|
98
|
+
features.unshift(version.to_s)
|
99
|
+
@features_state.write
|
100
|
+
Git.add(@features_state.path)
|
101
|
+
generate_features_yml
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def append_feature(version)
|
106
|
+
features.push(version.to_s)
|
107
|
+
@features_state.write
|
108
|
+
Git.add(@features_state.path)
|
109
|
+
generate_features_yml
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_features_yml
|
114
|
+
features_yml_file = File.join(features_dir, "features.yml")
|
115
|
+
Share.cp("release_migration/features.yml", features_yml_file)
|
116
|
+
file = File.open(features_yml_file, "a")
|
117
|
+
features.each { |feature|
|
118
|
+
version = Version.new(feature)
|
119
|
+
if base_version == version.truncate(:pre)
|
120
|
+
file.puts "- #{version.feature}"
|
121
|
+
else
|
122
|
+
file.puts "- ../#{version.truncate(:pre)}/#{version.feature}"
|
123
|
+
end
|
124
|
+
}
|
125
|
+
end
|
53
126
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
# puts "files: #{files.inspect}"
|
59
|
-
# puts "release_files: #{release_files.inspect}"
|
60
|
-
# puts "feature_versions: #{feature_versions.map(&:to_s).inspect}"
|
61
|
-
# puts "keep_file: #{keep_file}"
|
62
|
-
# yield if block_given?
|
63
|
-
# }
|
127
|
+
def migrate(database)
|
128
|
+
MigrationBuilder.new(database, features_dir).build
|
129
|
+
Rdbms.exec_sql(database.name, "delete from prick.versions", user: database.user)
|
130
|
+
Rdbms.exec_file(database.name, File.join(PRICK_DIR, "data.sql"), user: database.user)
|
64
131
|
end
|
65
132
|
|
66
|
-
|
67
|
-
|
68
|
-
def
|
133
|
+
# This only migrates included features. It is used in pre-releases to migrate included features
|
134
|
+
# before creating a diff, so the diff only contains changes not defined in the features
|
135
|
+
def migrate_features(database)
|
136
|
+
MigrationBuilder.new(database, features_dir).build("features.yml")
|
137
|
+
end
|
69
138
|
end
|
70
139
|
|
71
140
|
class ReleaseMigration < Migration
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
super(
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
def feature_versions() read_features_yml.map { |path| Migration.version(path) } end
|
96
|
-
def feature_paths() read_features_yml end
|
97
|
-
|
98
|
-
# `feature` is a Feature object
|
99
|
-
def include_feature(migration, append: true)
|
100
|
-
migration.is_a?(FeatureMigration) or raise "Expected FeatureMigration object, got #{migration.class}"
|
101
|
-
!feature_paths.include?(migration.path) or raise Error, "Feature #{migration.version} is already included"
|
102
|
-
exclude_files = [Schema.data_file] +
|
103
|
-
migration.release_files +
|
104
|
-
migration.feature_paths.map { |path| FeatureMigration.release_files(path) }.flatten
|
105
|
-
|
106
|
-
Git.merge_branch(migration.version, exclude_files: exclude_files)
|
107
|
-
|
108
|
-
feature_paths = YAML.load(Git.read(migration.features_yml, branch: migration.version))
|
109
|
-
append_features_yml(feature_paths, append: append)
|
110
|
-
|
111
|
-
feature_paths.each { |feature_path|
|
112
|
-
if path != File.dirname(feature_path)
|
113
|
-
FileUtils.ln_s(File.join("../..", feature_path), path)
|
114
|
-
Git.add(File.join(path, File.basename(feature_path)))
|
115
|
-
end
|
116
|
-
}
|
141
|
+
def version() @migration_state&.version end
|
142
|
+
def version=(v)
|
143
|
+
@migration_state ||= MigrationState.new(ReleaseMigration.migration_dir(v))
|
144
|
+
@migration_state.version = v
|
145
|
+
end
|
146
|
+
|
147
|
+
def base_version() @migration_state&.base_version end # || @migration_state.version end
|
148
|
+
def base_version=(v)
|
149
|
+
@migration_state or raise Internal, "Can't set base version when version is undefined"
|
150
|
+
@migration_state.base_version = v
|
151
|
+
end
|
152
|
+
|
153
|
+
# Note that `version` can be nil
|
154
|
+
def initialize(version, base_version)
|
155
|
+
migration_dir = version && ReleaseMigration.migration_dir(version)
|
156
|
+
features_dir = ReleaseMigration.features_dir(base_version)
|
157
|
+
super(migration_dir, features_dir)
|
158
|
+
if version
|
159
|
+
self.version = version
|
160
|
+
self.base_version = base_version
|
161
|
+
else
|
162
|
+
@base_migration_state = MigrationState.new(features_dir)
|
163
|
+
end
|
117
164
|
end
|
118
165
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
}
|
166
|
+
def create() super("release_migration/*") end
|
167
|
+
|
168
|
+
def self.load(migration_dir, features_dir = nil)
|
169
|
+
!migration_dir.nil? or raise "Parameter migration_dir can't be nil"
|
170
|
+
migration_state = MigrationState.new(migration_dir).read
|
171
|
+
if features_dir.nil?
|
172
|
+
features_dir = ReleaseMigration.features_dir(migration_state.base_version)
|
127
173
|
end
|
174
|
+
self.new(migration_state.version, migration_state.base_version)
|
128
175
|
end
|
176
|
+
|
177
|
+
def self.migration_dir(version) version && File.join(RELEASES_DIR, version.to_s) end
|
178
|
+
def self.features_dir(base_version) migration_dir(base_version) end
|
129
179
|
|
130
180
|
|
131
|
-
|
132
|
-
FILES.map { |file| File.join(path, file) }
|
133
|
-
end
|
181
|
+
end
|
134
182
|
|
135
|
-
|
183
|
+
class MigrationMigration < Migration
|
184
|
+
attr_reader :name
|
136
185
|
|
137
|
-
def
|
138
|
-
|
186
|
+
def initialize(version, base_version)
|
187
|
+
@name = "#{base_version}_#{version}"
|
188
|
+
dir = File.join(MIGRATIONS_DIR, name)
|
189
|
+
super(dir, dir)
|
190
|
+
self.version = version
|
191
|
+
self.base_version = base_version
|
139
192
|
end
|
140
193
|
|
141
|
-
def
|
142
|
-
FileUtils.cp(template_file(features_yml), features_yml)
|
143
|
-
File.open(features_yml, "a") { |f| f.write(paths.to_yaml) }
|
144
|
-
FileUtils.cp(template_file(features_sql), features_sql)
|
145
|
-
File.open(features_sql, "a") { |f|
|
146
|
-
paths.map { |path|
|
147
|
-
f.puts "\\cd #{File.basename(path)}"
|
148
|
-
f.puts "\\i migrate.sql" }
|
149
|
-
f.puts "\\cd .."
|
150
|
-
f.puts
|
151
|
-
}
|
152
|
-
Git.add(features_yml)
|
153
|
-
Git.add(features_sql)
|
154
|
-
end
|
194
|
+
def create() super("release_migration/*") end
|
155
195
|
end
|
156
196
|
|
157
197
|
class FeatureMigration < Migration
|
158
|
-
FEATURE_TMPL_DIR = "features/feature"
|
159
|
-
FILES = [
|
160
|
-
MIGRATE_SQL = "migrate.sql",
|
161
|
-
DIFF_SQL = "diff.sql"
|
162
|
-
]
|
163
|
-
|
164
198
|
attr_reader :name
|
165
|
-
attr_reader :version
|
166
|
-
attr_reader :release_migration # The enclosing release migration
|
167
|
-
|
168
|
-
attr_reader :migrate_sql
|
169
|
-
attr_reader :diff_sql
|
170
|
-
|
171
|
-
def features_yml() release_migration.features_yml end
|
172
|
-
def features_sql() release_migration.features_sql end
|
173
|
-
def migrations_sql() release_migration.migrations_sql end
|
174
|
-
def release_diff_sql() release_migration.diff_sql end
|
175
|
-
|
176
|
-
def files() [migrate_sql, diff_sql] end
|
177
|
-
def release_files() release_migration.files end
|
178
199
|
|
179
|
-
def
|
180
|
-
def feature_paths() release_migration.feature_paths end
|
181
|
-
|
182
|
-
def initialize(path, name: File.basename(path))
|
183
|
-
Migration.feature?(path) or raise "Expected a feature path, got #{path}"
|
184
|
-
super(path, FEATURE_TMPL_DIR)
|
200
|
+
def initialize(name, base_version)
|
185
201
|
@name = name
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
end
|
191
|
-
|
192
|
-
def exist?() release_migration.exist? && super end
|
193
|
-
def create()
|
194
|
-
release_migration.exist? || release_migration.create
|
195
|
-
super
|
196
|
-
end
|
197
|
-
|
198
|
-
def present?() release_migration.present? && super end
|
199
|
-
def prepare()
|
200
|
-
release_migration.present? || release_migration.prepare
|
201
|
-
super
|
202
|
-
release_migration.append_features_yml([path])
|
203
|
-
end
|
204
|
-
|
205
|
-
def include_feature(migration)
|
206
|
-
release_migration.include_feature(migration, append: false)
|
207
|
-
end
|
208
|
-
|
209
|
-
def self.files(path)
|
210
|
-
release_files + FILES.map { |file| File.join(path, file) }
|
202
|
+
dir = File.join(RELEASES_DIR, base_version.to_s, name)
|
203
|
+
super(dir, dir)
|
204
|
+
self.version = Version.new(base_version, feature: name)
|
205
|
+
self.base_version = base_version
|
211
206
|
end
|
212
207
|
|
213
|
-
def
|
214
|
-
ReleaseMigration.files(path)
|
215
|
-
end
|
216
|
-
|
217
|
-
def dump
|
218
|
-
super {
|
219
|
-
puts "name: #{name}"
|
220
|
-
puts "release_migration: #{path}"
|
221
|
-
}
|
222
|
-
end
|
208
|
+
def create() super("feature_migration/*") end
|
223
209
|
end
|
224
210
|
end
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|