prick 0.3.0 → 0.8.0

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