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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -5
  3. data/Gemfile +4 -1
  4. data/TODO +3 -0
  5. data/doc/prick.txt +114 -0
  6. data/exe/prick +224 -370
  7. data/lib/ext/fileutils.rb +11 -0
  8. data/lib/ext/forward_method.rb +18 -0
  9. data/lib/ext/shortest_path.rb +44 -0
  10. data/lib/prick.rb +17 -9
  11. data/lib/prick/branch.rb +254 -0
  12. data/lib/prick/builder.rb +141 -0
  13. data/lib/prick/command.rb +19 -11
  14. data/lib/prick/constants.rb +42 -20
  15. data/lib/prick/database.rb +5 -3
  16. data/lib/prick/diff.rb +47 -0
  17. data/lib/prick/exceptions.rb +15 -3
  18. data/lib/prick/git.rb +46 -21
  19. data/lib/prick/migration.rb +165 -185
  20. data/lib/prick/program.rb +238 -0
  21. data/lib/prick/project.rb +266 -358
  22. data/lib/prick/rdbms.rb +2 -2
  23. data/lib/prick/schema.rb +19 -88
  24. data/lib/prick/share.rb +64 -0
  25. data/lib/prick/state.rb +137 -0
  26. data/lib/prick/version.rb +34 -14
  27. data/libexec/strip-comments +33 -0
  28. data/make_releases +48 -345
  29. data/make_schema +10 -0
  30. data/prick.gemspec +11 -22
  31. data/share/feature_migration/diff.sql +2 -0
  32. data/share/feature_migration/migrate.sql +2 -0
  33. data/share/release_migration/diff.sql +3 -0
  34. data/share/release_migration/features.yml +6 -0
  35. data/share/release_migration/migrate.sql +5 -0
  36. data/share/release_migration/migrate.yml +5 -0
  37. data/share/schema/build.yml +14 -0
  38. data/share/schema/schema.sql +5 -0
  39. data/share/schemas/build.yml +3 -0
  40. data/share/schemas/prick/build.yml +14 -0
  41. data/share/schemas/prick/data.sql +1 -2
  42. data/share/schemas/prick/schema.sql +0 -15
  43. data/share/schemas/prick/tables.sql +17 -0
  44. data/share/schemas/public/.keep +0 -0
  45. data/share/schemas/public/build.yml +14 -0
  46. data/share/schemas/public/schema.sql +3 -0
  47. data/test_assorted +192 -0
  48. data/test_feature +112 -0
  49. data/test_single_dev +83 -0
  50. 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
+
@@ -1,442 +1,350 @@
1
+ require "prick/state.rb"
1
2
 
2
- require "prick/constants.rb"
3
-
4
- require "prick/archive.rb"
5
- require "prick/build.rb"
6
- require "prick/migration.rb"
7
- require "prick/database.rb"
8
- require "prick/schema.rb"
9
- require "prick/git.rb"
10
- require "prick/migra.rb"
11
-
12
- require 'fileutils'
3
+ require "tmpdir"
13
4
 
14
5
  module Prick
15
6
  class Project
16
- # Name of project. Used in database and release names. Defaults to the name
17
- # of the current directory
7
+ # Name of project. Persisted in the project state file
18
8
  attr_reader :name
19
9
 
20
- # Name of Postgresql user that own the database(s). Defaults to #name
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
- # Schema. Represents the schema definition under the schemas/ directory
24
- attr_reader :schema
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
- # List of all builds
34
- def builds() @builds_by_name.values end
35
-
36
- # List of releases ordered from oldest to newest. Custom releases are sorted
37
- # after regular releases and alphabetical between themselves
38
- attr_reader :releases
39
-
40
- # List of pre-releases
41
- attr_reader :prereleases
20
+ # Project (default) database
21
+ def database(version = nil)
22
+ version ? Database.new("#{name}-#{version.truncate(:pre)}", user) : @database
23
+ end
42
24
 
43
- # Hash from base-release name to feature. Note that this doesn't follow release history
44
- attr_reader :features
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
- # Ignored and orphaned objects
47
- attr_reader :ignored_feature_nodes
48
- attr_reader :ignored_release_nodes
49
- attr_reader :orphan_feature_nodes
50
- attr_reader :orphan_release_nodes
51
- attr_reader :orphan_git_branches
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
- # True if we're on a release branch
54
- def release?() release && release.version.pre.nil? && !feature? end
66
+ def self.initialized?(directory)
67
+ File.directory?(directory) && Dir.chdir(directory) { ProjectState.new.exist? }
68
+ end
55
69
 
56
- # True if we're on a pre-release branch
57
- def prerelease?() release && !release.version.pre.nil? && !feature? end
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
- # True if we're on a feature branch
60
- def feature?() release && !release.version.feature.nil? end
76
+ # Create prick version file
77
+ PrickVersion.new.write(VERSION)
61
78
 
62
- # Sorted list of databases associated with a release. Also include the
63
- # project database if project. is true. If all: is true, also list
64
- # databases that match the project but is without an associated release
65
- def databases(all: false, project: true)
66
- r = (project ? [database] : [])
67
- if all
68
- r + Rdbms.list_databases(Prick.database_re(name)).map { |name| Database.new(name, user) }
69
- else
70
- r + @releases.map(&:database)
71
- end
72
- end
79
+ # Create project state file
80
+ ProjectState.new(name: name, user: user).write
73
81
 
74
- # `name` is the project name and `user` is the name of the Postgresql user.
75
- # `name` defaults to the name of the current directory and `user` defaults
76
- # to `name`
77
- def initialize(name = nil, user = nil)
78
- @name = name || File.basename(Dir.getwd)
79
- @name =~ PROJECT_NAME_RE or raise Error, "Illegal project name: #@name"
80
- @user = user || @name
81
- @user =~ USER_NAME_RE or raise Error, "Illegal postgres user name: #@user"
82
- # Rdbms.ensure_state(:user_exist, @user)
83
- Rdbms.create_user(@user) if !Rdbms.exist_user?(@user) # FIXME
84
- @schema = Schema.new(self)
85
- @database = Database.new(@name, @user)
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
- @builds_by_name = {}
88
- @builds_by_version = {}
89
+ # Copy default gitignore and schema files
90
+ Share.cp("gitignore", ".gitignore")
91
+ Share.cp("schemas", ".")
89
92
 
90
- @releases = []
91
- @prereleases = []
92
- @features = {}
93
+ # Add everything so far
94
+ Git.add(".")
95
+ Git.commit("Initial import")
93
96
 
94
- @ignored_feature_nodes = []
95
- @ignored_release_nodes = []
97
+ # Create initial release
98
+ release = Release.new(Version.zero, nil)
99
+ release.create
96
100
 
97
- @orphan_feature_nodes = []
98
- @orphan_release_nodes = [] # Includes prereleases
101
+ schema = Schema.new(SCHEMAS_DIR)
102
+ schema.version = release.version
103
+ Git.add schema.version_file
99
104
 
100
- @orphan_git_branches = []
105
+ Git.commit "Release 0.0.0"
106
+ release.tag!
101
107
 
102
- load_project
108
+ # Kill master branch
109
+ Git.delete_branch("master")
110
+ }
103
111
  end
104
112
 
105
- def [](name_or_version)
106
- name = version = name_or_version
107
- case name_or_version
108
- when String; @builds_by_name[name]
109
- when Version; @builds_by_version[version]
110
- when NilClass; nil
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
- raise Internal, "Expected String or Version index, got #{name_or_version.class}"
126
+ branch.build(database)
113
127
  end
128
+ self
114
129
  end
115
130
 
116
- def []=(name_or_version, branch)
117
- case name_or_version
118
- when String;
119
- name = name_or_version
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 dirty?() !Git.clean? end
131
-
132
- def build?(name_or_version)
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
- # Initialize an on-disk prick instance
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
- FileUtils.mkdir_p(DIRS)
152
- DIRS.each { |dir| FileUtils.touch("#{dir}/.keep") }
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
- # Create an initial release 0.0.0. ::initialize_project expects to be executed
157
- # in the project directory
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
- # Returns the full path to the project directory (TODO: Test)
173
- def path() File.expand_path(".") end
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
- # Migrate the database from its current version to the current release on disk
183
- def migrate
184
- check_clean
185
- database.exist? or raise "Project database not found"
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
- ### DEVELOPER-LEVEL METHODS
193
-
194
- # Create and switch to feature branch
195
- def feature(name)
196
- check_clean
197
- release? or raise Error, "Need to be on a release branch to create a feature"
198
- feature = Feature.new(self, release, name).ensure(:active)
199
- Git.commit "Created feature #{name}"
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 rebase(version)
203
- check_clean
204
- feature? or raise Error, "Need to be on a feature branch to rebase"
205
- release.rebase(version)
206
- Git.commit "Rebased to #{version}"
207
- end
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
- ### RELEASE MANAGER LEVEL METHODS
179
+ to_name = "#{name}-next"
180
+ to_database = Database.new(to_name, user)
181
+ build(database: to_database)
210
182
 
211
- # Create the first prerelease
212
- #
213
- # TODO Make it just copy files and commit them. Supports a single-user
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
- def prepare_migration
222
- check_clean
223
- prerelease? || feature? or raise Error, "Need to be on a prerelease or feature branch to include features"
187
+ if release?
188
+ dir = branch.migration.migration_dir
189
+ else
190
+ dir = branch.migration.features_dir
191
+ end
224
192
 
225
- base_database = release.base_release.database
226
- build(base_release.version) if !base_database.loaded?
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
- diff_sql = release.migration.diff_sql
230
- Migra.migrate(base_database.name, database.name, diff_sql)
231
- end
196
+ if prerelease?
197
+ branch.migrate_features(from_database)
198
+ end
232
199
 
233
- def create_release(version = nil)
234
- check_clean
235
- if release?
236
- !version.nil? or raise Error, "Need version argument when on a release branch"
237
- new_release = Release.new(self, release, version)
238
- elsif prerelease?
239
- version.nil? or raise Error, "Can't use version argument when on a pre-release branch"
240
- new_release = release.target_release
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 create_prerelease(target_version)
249
- check_clean
250
- release? or raise Error, "Need to be on a release branch to create a pre-release"
251
- prerelease = Prerelease.new(self, release, target_version.increment(:pre, 0), target_version)
252
- prerelease.create
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 increment_prerelease
256
- check_clean
257
- prerelease? or raise Error, "Need to be on a pre-release branch to make an incremental pre-release"
258
- prerelease = Prerelease.new(self, release.base_release, release.version.increment(:pre))
259
- prerelease.create
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 create_feature(name)
263
- check_clean
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
- # Note: Does not commit
270
- def include_feature(feature_version)
271
- check_clean
272
- prerelease? || feature? or raise Error, "Need to be on a prerelease or feature branch to include features"
273
- Git.branch?(feature_version) or raise Error, "Can't find branch #{feature_version}"
274
- release.include_feature(self[feature_version])
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 commit_feature
278
- msg = File.readlines(".git/MERGE_MSG").first.chomp
279
- Git.commit msg
280
- # Command.command "git commit -F .git/MERGE_MSG"
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 create_migration
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
- # Build the current database from the content of schema
287
- def build(version = nil)
288
- if version.nil?
289
- database.recreate
290
- schema.build
291
- else
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
- # Load file into current database
310
- def use_database(file = nil)
311
- check_clean
312
- File.file?(file) or raise Error, "Can't find #{file}"
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
- # Load file into the given database version. If version is nil the current
318
- # database is used. If file is nil, the database associated with the given
319
- # version is used. Not both version and file can be nil
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
- # def reload_release(version, *file)
336
- # lookup(version).reload(*file)
337
- # end
338
- #
339
- # def unload(version = nil)
340
- # if version
341
- # lookup(version).database.drop
342
- # else
343
- # databases.each(&:drop)
344
- # end
345
- # end
346
- #
347
- # Remove temporary files and databases
348
- def cleanup(version = nil, project: false)
349
- if version
350
- release = self[version]
351
- release.archive.delete
352
- release.database.drop
353
- else
354
- FileUtils::rm_rf CLONE_DIR
355
- FileUtils::rm_f Dir.glob(File.join(CACHE_DIR, Prick.dump_glob(name)))
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
- private
361
- def check_clean()
362
- !dirty? or raise "Repository is dirty. Commit your changes and try again"
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
- # For debug
366
- def dump_deps(deps)
367
- deps.each { |k,v|
368
- puts " #{k} -> #{v || 'nil'}"
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
- def load_project
373
- # Maps from version to base release version
374
- releases = { Version.zero => nil }
375
- prereleases = {}
376
- features = {}
377
-
378
- # Collect branches from git
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
- # Find base release for present releases and prereleases
397
- Dir.each_child(RELEASE_DIR) { |node|
398
- next if node.start_with?(".")
399
- if Version.version?(node)
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
- releases.sort_by { |k,v| k }.each { |release, base_release|
416
- @releases << Release.new(self, self[base_release], release)
417
- }
418
- @releases.sort!
337
+ private
338
+ def submit(msg, commit = true)
339
+ Git.commit msg if commit
340
+ @message = msg
341
+ end
419
342
 
420
- prereleases.each { |prerelease, base_release|
421
- if base_release
422
- @prereleases << Prerelease.new(self, self[base_release], prerelease)
423
- else
424
- @ignored_release_nodes << prerelease
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