prick 0.3.0 → 0.8.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/TODO +7 -0
  4. data/exe/prick +235 -163
  5. data/lib/ext/fileutils.rb +7 -0
  6. data/lib/prick.rb +7 -5
  7. data/lib/prick/builder.rb +31 -8
  8. data/lib/prick/cache.rb +34 -0
  9. data/lib/prick/constants.rb +106 -54
  10. data/lib/prick/database.rb +26 -20
  11. data/lib/prick/diff.rb +103 -25
  12. data/lib/prick/git.rb +31 -9
  13. data/lib/prick/head.rb +183 -0
  14. data/lib/prick/migration.rb +41 -181
  15. data/lib/prick/program.rb +255 -0
  16. data/lib/prick/project.rb +264 -0
  17. data/lib/prick/rdbms.rb +3 -12
  18. data/lib/prick/schema.rb +6 -11
  19. data/lib/prick/state.rb +129 -74
  20. data/lib/prick/version.rb +41 -28
  21. data/prick.gemspec +3 -1
  22. data/share/diff/diff.after-tables.sql +4 -0
  23. data/share/diff/diff.before-tables.sql +4 -0
  24. data/share/diff/diff.tables.sql +8 -0
  25. data/share/migration/diff.tables.sql +8 -0
  26. data/share/{release_migration → migration}/features.yml +0 -0
  27. data/share/migration/migrate.sql +3 -0
  28. data/share/{release_migration → migration}/migrate.yml +3 -0
  29. data/share/migration/tables.sql +3 -0
  30. data/share/{schemas → schema/schema}/build.yml +0 -0
  31. data/share/{schemas → schema/schema}/prick/build.yml +0 -0
  32. data/share/schema/schema/prick/data.sql +7 -0
  33. data/share/{schemas → schema/schema}/prick/schema.sql +0 -0
  34. data/share/{schemas → schema/schema}/prick/tables.sql +2 -2
  35. data/share/{schemas → schema/schema}/public/.keep +0 -0
  36. data/share/{schemas → schema/schema}/public/build.yml +0 -0
  37. data/share/{schemas → schema/schema}/public/schema.sql +0 -0
  38. data/test_refactor +34 -0
  39. metadata +23 -21
  40. data/file +0 -0
  41. data/lib/prick/build.rb +0 -376
  42. data/lib/prick/migra.rb +0 -22
  43. data/share/feature_migration/diff.sql +0 -2
  44. data/share/feature_migration/migrate.sql +0 -2
  45. data/share/release_migration/diff.sql +0 -3
  46. data/share/release_migration/migrate.sql +0 -5
  47. data/share/schemas/prick/data.sql +0 -7
data/lib/prick/diff.rb CHANGED
@@ -1,47 +1,125 @@
1
1
  module Prick
2
2
  class Diff
3
+ # Diff as a list of lines
4
+ attr_reader :diff
5
+
6
+ # Subject of the diff
7
+ attr_reader :before_table_changes
8
+ attr_reader :table_changes
9
+ attr_reader :after_table_changes
10
+
3
11
  def initialize(db1, db2)
4
12
  @db1, @db2 = db1, db2
5
- @diffed = false
6
- @diff = nil
7
- @result = nil
13
+ do_diff
14
+ split_on_table_changes
8
15
  end
9
16
 
10
17
  def self.same?(db1, db2) Diff.new(db1, db2).same? end
11
18
 
12
19
  # Return true if the two databases are equal. Named #same? to avoid name
13
20
  # collision with the built in #equal?
14
- def same?
15
- do_diff if @result.nil?
16
- @result
17
- end
21
+ def same?() diff.empty? end
18
22
 
19
- # Return the diff between the two databases or nil if they're equal
20
- def read
21
- @diffed ? @diff : do_diff
23
+ # Write the diff between the databases to the given file(s). Return true if
24
+ # the databases are equal
25
+ def write_segments(file1, file2 = nil, file3 = nil, mark: true)
26
+ if file2.nil? && file3.nil?
27
+ file2 = file3 = file1
28
+ elsif file2.nil? ^ !file3.nil?
29
+ raise Internal, "Either none or both of `file2` and `file3` should be nil"
30
+ end
31
+ write_segment(file1, :before_table_changes, mark: mark)
32
+ write_segment(file2, :table_changes, mark: mark)
33
+ write_segment(file3, :after_table_changes, mark: mark)
22
34
  end
23
35
 
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)
36
+ def write_segment(file, segment, mark: true)
37
+ lines = self.send(segment)
38
+ if lines.empty?
39
+ FileUtils.rm_f(file) if file != "/dev/stdout"
29
40
  else
30
- do_diff(file)
41
+ File.open(file, "w") { |f|
42
+ f.puts "--" + segment.to_s.gsub("_", " ").upcase if mark
43
+ f.puts lines
44
+ }
31
45
  end
32
- @result
33
46
  end
34
47
 
35
48
  private
36
49
  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
50
+ command = "migra --unsafe --with-privileges postgres:///#{@db1} postgres:///#{@db2}"
51
+ @diff = Command.command(command, fail: false)
52
+ [0,2].include?(Command.status) or
53
+ raise Internal, "migrate command failed with status #{Command.status}: #{command}"
54
+ end
55
+
56
+ # Initialize table change variables
57
+ def split_on_table_changes
58
+ @before_table_changes = []
59
+ @table_changes = []
60
+ @after_table_changes = []
61
+
62
+ before = true
63
+ tables = false
64
+ after = false
65
+
66
+ in_table = false
67
+
68
+ diff.each { |l|
69
+ if before
70
+ if l =~ /^alter table/
71
+ @table_changes << l
72
+ before = false
73
+ tables = true
74
+ elsif l =~ /^create table/
75
+ @table_changes << l
76
+ before = false
77
+ tables = true
78
+ in_table = true
79
+ elsif l =~ /^drop table/
80
+ @table_changes << l
81
+ before = false
82
+ tables = true
83
+ else
84
+ @before_table_changes << l
85
+ end
86
+ elsif tables
87
+ if in_table
88
+ if l =~ /^\)/
89
+ @table_changes << l
90
+ in_table = false
91
+ else
92
+ @table_changes << l
93
+ end
94
+ elsif l =~ /^alter table/
95
+ @table_changes << l
96
+ elsif l =~ /^create table/
97
+ @table_changes << l
98
+ in_table = true
99
+ elsif l =~ /^drop table/
100
+ @table_changes << l
101
+ elsif l =~ /^\s*$/
102
+ @table_changes << l
103
+ else
104
+ @after_table_changes << l
105
+ tables = false
106
+ after = true
107
+ end
108
+ else
109
+ @after_table_changes << l
110
+ end
111
+ }
45
112
  end
46
113
  end
47
114
  end
115
+
116
+
117
+
118
+
119
+
120
+
121
+
122
+
123
+
124
+
125
+
data/lib/prick/git.rb CHANGED
@@ -88,6 +88,11 @@ module Prick
88
88
  Command.command "git branch #{name}"
89
89
  end
90
90
 
91
+ # Rename a branch
92
+ def self.rename_branch(from, to)
93
+ Command.command "git branch -m #{from} #{to}"
94
+ end
95
+
91
96
  # Destroy branch
92
97
  def self.delete_branch(name)
93
98
  Command.command "git branch -D #{name}", fail: false
@@ -127,9 +132,13 @@ module Prick
127
132
  merge_branch(name, exclude_files: exclude_files, fail: fail)
128
133
  end
129
134
 
130
- # List branches
131
- def self.list_branches
132
- Command.command "git branch --format='%(refname:short)'"
135
+ # List branches. Detached head "branches" are ignored unless :detached_head is true
136
+ def self.list_branches(detached_head: false)
137
+ if detached_head
138
+ Command.command "git branch --format='%(refname:short)'"
139
+ else
140
+ Command.command "git for-each-ref --format='%(refname:short)' refs/heads/*"
141
+ end
133
142
  end
134
143
 
135
144
  # Add a file to the index of the current branch
@@ -141,6 +150,14 @@ module Prick
141
150
  }
142
151
  end
143
152
 
153
+ def self.changed?(file)
154
+ Command.command("git status --porcelain").any? { |l| l =~ /^.M #{file}$/ }
155
+ end
156
+
157
+ def self.added?(file)
158
+ Command.command("git status --porcelain").any? { |l| l =~ /^A. #{file}$/ }
159
+ end
160
+
144
161
  # Return content of file in the given tag or branch. Defaults to HEAD
145
162
  def self.readlines(file, tag: nil, branch: nil)
146
163
  !(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
@@ -163,15 +180,20 @@ module Prick
163
180
  end.join("\n") + "\n"
164
181
  end
165
182
 
166
- def self.rm(file)
167
- Dir.chdir(File.dirname(file)) {
168
- Command.command "git rm -f '#{File.basename(file)}'", fail: false
183
+ def self.rm(*files)
184
+ Array(files).flatten.each { |file|
185
+ Dir.chdir(File.dirname(file)) {
186
+ Command.command "git rm -f '#{File.basename(file)}'", fail: false
187
+ }
169
188
  }
170
189
  end
171
190
 
172
- def self.rm_rf(file)
173
- Dir.chdir(File.dirname(file)) {
174
- Command.command "git rm -rf '#{File.basename(file)}'", fail: false
191
+ def self.rm_rf(*files)
192
+ Array(files).flatten.each { |file|
193
+ Dir.chdir(File.dirname(file)) {
194
+ next if file == ".keep"
195
+ Command.command "git rm -rf '#{File.basename(file)}'", fail: false
196
+ }
175
197
  }
176
198
  end
177
199
 
data/lib/prick/head.rb ADDED
@@ -0,0 +1,183 @@
1
+
2
+ module Prick
3
+ class Head
4
+ def project_name() Prick.project.name end
5
+
6
+ # Usually equal to #version.to_s
7
+ attr_reader :name
8
+
9
+ # Version. This should be equal to schema.version
10
+ attr_reader :version
11
+
12
+ def base_version() @migration.base_version end
13
+
14
+ attr_reader :schema
15
+
16
+ attr_reader :migration
17
+
18
+ def clean?() Git::clean? end
19
+
20
+ def tag?() false end
21
+ def release_tag?() false end
22
+ def migration_tag?() false end
23
+
24
+ def branch?() false end
25
+ def release_branch?() false end
26
+ def prerelease_branch?() false end
27
+ def feature_branch?() false end
28
+ def migration_branch?() false end
29
+
30
+ def initialize(name, version, migration)
31
+ @name = name
32
+ @version = version
33
+ @schema = Schema.new
34
+ @migration = migration
35
+ end
36
+
37
+ # TODO: Handle migrations
38
+ def self.load(name)
39
+ version, tag = Version.parse(name)
40
+ if tag
41
+ ReleaseTag.load
42
+ else
43
+ if version.release?
44
+ ReleaseBranch.load
45
+ elsif version.prerelease?
46
+ PrereleaseBranch.load(version)
47
+ elsif version.feature?
48
+ FeatureBranch.load(version)
49
+ else
50
+ raise NotHere
51
+ end
52
+ end
53
+ end
54
+
55
+ def create()
56
+ raise NotThis
57
+ end
58
+
59
+ def build(database)
60
+ schema.build(database)
61
+ end
62
+
63
+ def self.database_name(version)
64
+ version.truncate(:pre).to_s
65
+ end
66
+ end
67
+
68
+ class Tag < Head
69
+ def tag?() true end
70
+
71
+ def initialize(version, base_version, migration = nil)
72
+ migration ||= Migration.new(version, base_version)
73
+ super(version.to_s, version, migration)
74
+ end
75
+
76
+ def self.load
77
+ state = Migration.load
78
+ self.new(state.version, state.base_version)
79
+ end
80
+
81
+ def create
82
+ initial_branch_name = "#{version}_initial"
83
+ clean? or raise Internal, "Repository is not clean"
84
+ !Git.detached? or raise Internal, "Not on a branch"
85
+ Git.create_branch(initial_branch_name)
86
+ Git.checkout_branch(initial_branch_name)
87
+ migration.update(version)
88
+ schema.version = version
89
+ Git.commit "Created release v#{version}"
90
+ Git.create_tag(version)
91
+ Git.checkout_tag(version)
92
+ self
93
+ end
94
+ end
95
+
96
+ class ReleaseTag < Tag
97
+ def release_tag?() true end
98
+ end
99
+
100
+ # TODO
101
+ class PrereleaseTag < Tag
102
+ end
103
+
104
+ class MigrationTag < Tag
105
+ def migration_tag?() true end
106
+ end
107
+
108
+ class Branch < Head
109
+ def branch?() true end
110
+
111
+ def initialize(name, version, migration)
112
+ super(name, version, migration)
113
+ end
114
+
115
+ def create()
116
+ Git.create_branch(version)
117
+ Git.checkout_branch(version)
118
+ migration.create
119
+ self
120
+ end
121
+ end
122
+
123
+ class ReleaseBranch < Branch
124
+ def release_branch?() true end
125
+
126
+ def initialize(fork = nil, base_version)
127
+ if fork
128
+ version = Version.new(base_version, fork: fork)
129
+ else
130
+ version = base_version
131
+ end
132
+ super(version.to_s, version, Migration.new(nil, base_version))
133
+ end
134
+
135
+ def self.load
136
+ state = MigrationState.load
137
+ ReleaseBranch.new(nil, state.base_version)
138
+ end
139
+ end
140
+
141
+ class PrereleaseBranch < Branch
142
+ def prerelease_branch?() true end
143
+
144
+ def initialize(version, base_version)
145
+ target_version = version.truncate(:pre)
146
+ super(version.to_s, target_version, Migration.new(target_version, base_version))
147
+ end
148
+
149
+ def self.load
150
+ state = MigrationState.load
151
+ PrereleaseBranch.new(state.version, state.base_version)
152
+ end
153
+ end
154
+
155
+ class FeatureBranch < Branch
156
+ def feature_branch?() true end
157
+
158
+ def initialize(feature_name, base_version)
159
+ name = Version.new(base_version, feature: feature_name).to_s
160
+ super(name, base_version, FeatureMigration.new(feature_name, base_version))
161
+ end
162
+
163
+ def self.load
164
+ state = MigrationState.load
165
+ FeatureBranch.new(state.version.feature)
166
+ end
167
+ end
168
+
169
+ # TODO: Versioned migrations (or maybe not)
170
+ class MigrationBranch < Branch
171
+ def migrationn_branch?() true end
172
+
173
+ def initialize(version, base_version)
174
+ if (version.fork || "") == (base_version.fork || "")
175
+ name = version.to_s + "-" + base_version.semver.to_s
176
+ else
177
+ name = version.to_s + "-" + base_version.to_s
178
+ end
179
+ super(name, version, Migration.new(version, base_version))
180
+ end
181
+ end
182
+ end
183
+
@@ -1,210 +1,70 @@
1
1
 
2
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
- #
6
3
  class Migration
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
4
+ attr_reader :version
5
+ attr_reader :base_version
10
6
 
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
16
-
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
20
-
21
- # Migration file
22
- def migrations_file() File.join(dir, MIGRATIONS_FILE) end
23
-
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
28
-
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
33
-
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
37
-
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)
7
+ def initialize(version, base_version)
8
+ @version = version
9
+ @base_version = base_version
42
10
  end
43
11
 
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
- }
12
+ def self.load
13
+ state = MigrationState.load
14
+ Migration.new(state.version, state.base_version)
52
15
  end
53
16
 
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
57
-
58
- def load
59
- @migration_state&.read
60
- @features_state.read
61
- self
17
+ # Remove content of the migration/ directory
18
+ def self.clear
19
+ FileUtils.empty!(MIGRATION_DIR)
62
20
  end
63
21
 
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
70
- end
22
+ def exist?() MigrationState.exist? end
71
23
 
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?)
77
- end
78
-
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
24
+ def create()
25
+ files = Share.cp "migration/*", MIGRATION_DIR
26
+ state = MigrationState.new.write(version: version, base_version: base_version)
27
+ Git.add files, state.path
94
28
  self
95
29
  end
96
30
 
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
31
+ def update(version)
32
+ state = MigrationState.new.read
33
+ @version = state.version = version
34
+ state.write
35
+ Git.add state.path
102
36
  self
103
37
  end
104
38
 
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
126
-
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)
131
- end
132
-
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")
39
+ def migrate(database)
40
+ base_version or raise Internal, "Can't migrate from nil to #{version}"
41
+ version or raise Internal, "Can't migrate from #{base_version} to nil"
42
+ MigrationBuilder.new(database, MIGRATION_DIR).build
137
43
  end
138
44
  end
139
45
 
140
- class ReleaseMigration < Migration
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
46
+ class FeatureMigration < Migration
47
+ attr_reader :feature
146
48
 
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
49
+ def initialize(feature, base_version)
50
+ super(base_version, base_version)
51
+ @feature = feature
151
52
  end
152
53
 
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
54
+ def self.load
55
+ migration_state = MigrationState.load
56
+ feature_state = FeatureState.load
57
+ FeatureMigration.new(feature_state.feature, migration_state.base_version)
164
58
  end
165
59
 
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)
173
- end
174
- self.new(migration_state.version, migration_state.base_version)
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
179
-
180
-
181
- end
182
-
183
- class MigrationMigration < Migration
184
- attr_reader :name
185
-
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
60
+ def create()
61
+ super
62
+ files = Share.cp "migration", File.join(MIGRATION_DIR, feature)
63
+ state = FeatureState.write(feature: feature)
64
+ Git.add files, state.path
65
+ self
192
66
  end
193
-
194
- def create() super("release_migration/*") end
195
67
  end
68
+ end
196
69
 
197
- class FeatureMigration < Migration
198
- attr_reader :name
199
-
200
- def initialize(name, base_version)
201
- @name = name
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
206
- end
207
70
 
208
- def create() super("feature_migration/*") end
209
- end
210
- end