prick 0.2.0 → 0.7.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 +6 -5
- data/Gemfile +4 -1
- data/TODO +10 -0
- data/doc/prick.txt +114 -0
- data/exe/prick +328 -402
- data/lib/ext/fileutils.rb +18 -0
- data/lib/ext/forward_method.rb +18 -0
- data/lib/ext/shortest_path.rb +44 -0
- data/lib/prick.rb +20 -10
- data/lib/prick/branch.rb +254 -0
- data/lib/prick/builder.rb +164 -0
- data/lib/prick/cache.rb +34 -0
- data/lib/prick/command.rb +19 -11
- data/lib/prick/constants.rb +122 -48
- data/lib/prick/database.rb +28 -20
- data/lib/prick/diff.rb +125 -0
- data/lib/prick/exceptions.rb +15 -3
- data/lib/prick/git.rb +77 -30
- data/lib/prick/head.rb +183 -0
- data/lib/prick/migration.rb +40 -200
- data/lib/prick/program.rb +493 -0
- data/lib/prick/project.rb +523 -351
- data/lib/prick/rdbms.rb +4 -13
- data/lib/prick/schema.rb +16 -90
- data/lib/prick/share.rb +64 -0
- data/lib/prick/state.rb +192 -0
- data/lib/prick/version.rb +62 -29
- data/libexec/strip-comments +33 -0
- data/make_releases +48 -345
- data/make_schema +10 -0
- data/prick.gemspec +14 -23
- data/share/diff/diff.after-tables.sql +4 -0
- data/share/diff/diff.before-tables.sql +4 -0
- data/share/diff/diff.tables.sql +8 -0
- data/share/migration/diff.tables.sql +8 -0
- data/share/migration/features.yml +6 -0
- data/share/migration/migrate.sql +3 -0
- data/share/migration/migrate.yml +8 -0
- data/share/migration/tables.sql +3 -0
- data/share/schema/build.yml +14 -0
- data/share/schema/schema.sql +5 -0
- data/share/schema/schema/build.yml +3 -0
- data/share/schema/schema/prick/build.yml +14 -0
- data/share/schema/schema/prick/data.sql +7 -0
- data/share/schema/schema/prick/schema.sql +5 -0
- data/share/{schemas/prick/schema.sql → schema/schema/prick/tables.sql} +2 -5
- data/{file → share/schema/schema/public/.keep} +0 -0
- data/share/schema/schema/public/build.yml +14 -0
- data/share/schema/schema/public/schema.sql +3 -0
- data/test_assorted +192 -0
- data/test_feature +112 -0
- data/test_refactor +34 -0
- data/test_single_dev +83 -0
- metadata +43 -68
- data/lib/prick/build.rb +0 -376
- data/lib/prick/migra.rb +0 -22
- data/share/schemas/prick/data.sql +0 -8
    
        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
         | 
| @@ -69,6 +88,11 @@ module Prick | |
| 69 88 | 
             
                  Command.command "git branch #{name}"
         | 
| 70 89 | 
             
                end
         | 
| 71 90 |  | 
| 91 | 
            +
                # Rename a branch
         | 
| 92 | 
            +
                def self.rename_branch(from, to)
         | 
| 93 | 
            +
                  Command.command "git branch -m #{from} #{to}"
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 72 96 | 
             
                # Destroy branch
         | 
| 73 97 | 
             
                def self.delete_branch(name)
         | 
| 74 98 | 
             
                  Command.command "git branch -D #{name}", fail: false
         | 
| @@ -94,28 +118,46 @@ module Prick | |
| 94 118 |  | 
| 95 119 | 
             
                  Command.command "git merge --no-commit #{name}", fail: false
         | 
| 96 120 |  | 
| 97 | 
            -
                  #  | 
| 98 | 
            -
                  files.each { |path, content| | 
| 121 | 
            +
                  # Restore excluded files
         | 
| 122 | 
            +
                  files.each { |path, content|
         | 
| 99 123 | 
             
                    File.open(path, "w") { |file| file.puts(content) }
         | 
| 100 124 | 
             
                    # Resolve git unmerged status
         | 
| 101 125 | 
             
                    Git.add(path)
         | 
| 102 126 | 
             
                  }
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  # TODO Detect outstanding merges
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def self.merge_tag(name, exclude_files: [], fail: false)
         | 
| 132 | 
            +
                  merge_branch(name, exclude_files: exclude_files, fail: fail)
         | 
| 103 133 | 
             
                end
         | 
| 104 134 |  | 
| 105 | 
            -
                # List branches
         | 
| 106 | 
            -
                def self.list_branches
         | 
| 107 | 
            -
                   | 
| 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
         | 
| 108 142 | 
             
                end
         | 
| 109 143 |  | 
| 110 144 | 
             
                # Add a file to the index of the current branch
         | 
| 111 145 | 
             
                def self.add(*files)
         | 
| 112 | 
            -
                  Array(files).each { |file|
         | 
| 146 | 
            +
                  Array(files).flatten.each { |file|
         | 
| 113 147 | 
             
                    Dir.chdir(File.dirname(file)) {
         | 
| 114 148 | 
             
                      Command.command "git add '#{File.basename(file)}'"
         | 
| 115 149 | 
             
                    }
         | 
| 116 150 | 
             
                  }
         | 
| 117 151 | 
             
                end
         | 
| 118 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 | 
            +
             | 
| 119 161 | 
             
                # Return content of file in the given tag or branch. Defaults to HEAD
         | 
| 120 162 | 
             
                def self.readlines(file, tag: nil, branch: nil)
         | 
| 121 163 | 
             
                  !(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
         | 
| @@ -127,7 +169,7 @@ module Prick | |
| 127 169 | 
             
                  end.map { |l| "#{l}\n" }
         | 
| 128 170 | 
             
                end
         | 
| 129 171 |  | 
| 130 | 
            -
                # Return content of file
         | 
| 172 | 
            +
                # Return content of file as a String
         | 
| 131 173 | 
             
                def self.read(file, tag: nil, branch: nil)
         | 
| 132 174 | 
             
                  !(tag && branch) or raise Internal, "Can't use both tag: and branch: options"
         | 
| 133 175 | 
             
                  if tag
         | 
| @@ -135,18 +177,23 @@ module Prick | |
| 135 177 | 
             
                  else
         | 
| 136 178 | 
             
                    branch ||= "HEAD"
         | 
| 137 179 | 
             
                    Command.command "git show #{branch}:#{file}"
         | 
| 138 | 
            -
                  end.join("\n")
         | 
| 180 | 
            +
                  end.join("\n") + "\n"
         | 
| 139 181 | 
             
                end
         | 
| 140 182 |  | 
| 141 | 
            -
                def self.rm( | 
| 142 | 
            -
                   | 
| 143 | 
            -
                     | 
| 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 | 
            +
                    }
         | 
| 144 188 | 
             
                  }
         | 
| 145 189 | 
             
                end
         | 
| 146 190 |  | 
| 147 | 
            -
                def self.rm_rf( | 
| 148 | 
            -
                   | 
| 149 | 
            -
                     | 
| 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 | 
            +
                    }
         | 
| 150 197 | 
             
                  }
         | 
| 151 198 | 
             
                end
         | 
| 152 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 | 
            +
             | 
    
        data/lib/prick/migration.rb
    CHANGED
    
    | @@ -1,230 +1,70 @@ | |
| 1 1 |  | 
| 2 | 
            -
            require 'yaml'
         | 
| 3 | 
            -
             | 
| 4 2 | 
             
            module Prick
         | 
| 5 3 | 
             
              class Migration
         | 
| 6 | 
            -
                 | 
| 7 | 
            -
             | 
| 8 | 
            -
                attr_reader :path
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                def files() raise AbstractMethod end
         | 
| 11 | 
            -
                def release_files() files end
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                # Return versions of the features in this migration
         | 
| 14 | 
            -
                def feature_versions() raise AbstractMethod end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                def initialize(path, template_dir)
         | 
| 17 | 
            -
                  @path = path
         | 
| 18 | 
            -
                  @template_dir = template_dir
         | 
| 19 | 
            -
                end
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                def exist?() File.exist?(keep_file) end
         | 
| 22 | 
            -
                def create() FileUtils.touch_p(keep_file); Git.add(keep_file) end
         | 
| 23 | 
            -
                def destroy() Git.rm_rf(path) end
         | 
| 24 | 
            -
             | 
| 25 | 
            -
                def present?() exist? && files.all? { |f| File.exist?(f) } end
         | 
| 26 | 
            -
                def prepare() files.each { |f| FileUtils.cp(template_file(f), path) }; Git.add(path) end
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                def include_feature(feature) raise AbstractMethod end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                def migrate() raise AbstractMethod end
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                def to_s() path end
         | 
| 33 | 
            -
                def <=>(other) path <=> other.path end
         | 
| 34 | 
            -
             | 
| 35 | 
            -
                def self.path(version)
         | 
| 36 | 
            -
                  if version.feature?
         | 
| 37 | 
            -
                    File.join(FEATURE_DIR, version.truncate(:feature).to_s, version.feature)
         | 
| 38 | 
            -
                  else
         | 
| 39 | 
            -
                    File.join(FEATURE_DIR, version)
         | 
| 40 | 
            -
                  end
         | 
| 41 | 
            -
                end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                def self.version(path)
         | 
| 44 | 
            -
                  Version.new(path.delete_prefix("#{FEATURE_DIR}/").sub("/", "_"))
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                def self.feature?(path)
         | 
| 48 | 
            -
                  Version.new(path.delete_prefix("#{FEATURE_DIR}/").sub("/", "_")).feature?
         | 
| 49 | 
            -
                end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                def self.files(path) raise AbstractMethod end
         | 
| 52 | 
            -
                def self.release_files(path) files(path) end
         | 
| 53 | 
            -
             | 
| 54 | 
            -
                def dump(&block)
         | 
| 55 | 
            -
            #     puts self.class
         | 
| 56 | 
            -
            #     indent {
         | 
| 57 | 
            -
            #       puts "path : #{path}"
         | 
| 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 | 
            -
            #     }
         | 
| 64 | 
            -
                end
         | 
| 65 | 
            -
             | 
| 66 | 
            -
              protected
         | 
| 67 | 
            -
                def template_file(path) File.join(SHARE_PATH, @template_dir, File.basename(path)) end
         | 
| 68 | 
            -
                def keep_file() File.join(path, KEEP_FILE) end
         | 
| 69 | 
            -
              end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
              class ReleaseMigration < Migration
         | 
| 72 | 
            -
                FEATURES_TMPL_DIR = "features"
         | 
| 73 | 
            -
                FILES = [
         | 
| 74 | 
            -
                  FEATURES_SQL = "features.sql",
         | 
| 75 | 
            -
                  FEATURES_YML = "features.yml",
         | 
| 76 | 
            -
                  MIGRATIONS_SQL = "migrations.sql",
         | 
| 77 | 
            -
                  DIFF_SQL = "diff.sql"
         | 
| 78 | 
            -
                ]
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                attr_reader :features_yml
         | 
| 81 | 
            -
                attr_reader :features_sql
         | 
| 82 | 
            -
                attr_reader :migrations_sql
         | 
| 83 | 
            -
                attr_reader :diff_sql
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                def files() [features_yml, features_sql, migrations_sql, diff_sql] end
         | 
| 4 | 
            +
                attr_reader :version
         | 
| 5 | 
            +
                attr_reader :base_version
         | 
| 86 6 |  | 
| 87 | 
            -
                def initialize( | 
| 88 | 
            -
                   | 
| 89 | 
            -
                  @ | 
| 90 | 
            -
                  @features_sql = File.join(path, FEATURES_SQL)
         | 
| 91 | 
            -
                  @migrations_sql = File.join(path, MIGRATIONS_SQL)
         | 
| 92 | 
            -
                  @diff_sql = File.join(path, DIFF_SQL)
         | 
| 7 | 
            +
                def initialize(version, base_version) 
         | 
| 8 | 
            +
                  @version = version
         | 
| 9 | 
            +
                  @base_version = base_version
         | 
| 93 10 | 
             
                end
         | 
| 94 11 |  | 
| 95 | 
            -
                def  | 
| 96 | 
            -
             | 
| 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 | 
            -
                  }
         | 
| 12 | 
            +
                def self.load
         | 
| 13 | 
            +
                  state = MigrationState.load
         | 
| 14 | 
            +
                  Migration.new(state.version, state.base_version)
         | 
| 117 15 | 
             
                end
         | 
| 118 16 |  | 
| 119 | 
            -
                 | 
| 120 | 
            -
             | 
| 121 | 
            -
                   | 
| 122 | 
            -
                    Dir.chdir(path) {
         | 
| 123 | 
            -
                      puts "  cd #{path}"
         | 
| 124 | 
            -
                      puts "  psql -d #{database_name} < #{MIGRATIONS_SQL}"
         | 
| 125 | 
            -
                      Command.command "psql -d #{database_name} < #{MIGRATIONS_SQL}"
         | 
| 126 | 
            -
                    }
         | 
| 127 | 
            -
                  end
         | 
| 17 | 
            +
                # Remove content of the migration/ directory
         | 
| 18 | 
            +
                def self.clear
         | 
| 19 | 
            +
                  FileUtils.empty!(MIGRATION_DIR)
         | 
| 128 20 | 
             
                end
         | 
| 129 21 |  | 
| 22 | 
            +
                def exist?() MigrationState.exist?  end
         | 
| 130 23 |  | 
| 131 | 
            -
                def  | 
| 132 | 
            -
                   | 
| 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
         | 
| 28 | 
            +
                  self
         | 
| 133 29 | 
             
                end
         | 
| 134 30 |  | 
| 135 | 
            -
                def  | 
| 136 | 
            -
             | 
| 137 | 
            -
             | 
| 138 | 
            -
                   | 
| 31 | 
            +
                def update(version)
         | 
| 32 | 
            +
                  state = MigrationState.new.read
         | 
| 33 | 
            +
                  @version = state.version = version
         | 
| 34 | 
            +
                  state.write
         | 
| 35 | 
            +
                  Git.add state.path
         | 
| 36 | 
            +
                  self
         | 
| 139 37 | 
             
                end
         | 
| 140 38 |  | 
| 141 | 
            -
                def  | 
| 142 | 
            -
                   | 
| 143 | 
            -
                   | 
| 144 | 
            -
                   | 
| 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)
         | 
| 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
         | 
| 154 43 | 
             
                end
         | 
| 155 44 | 
             
              end
         | 
| 156 45 |  | 
| 157 46 | 
             
              class FeatureMigration < Migration
         | 
| 158 | 
            -
                 | 
| 159 | 
            -
                FILES = [
         | 
| 160 | 
            -
                  MIGRATE_SQL = "migrate.sql",
         | 
| 161 | 
            -
                  DIFF_SQL = "diff.sql"
         | 
| 162 | 
            -
                ]
         | 
| 47 | 
            +
                attr_reader :feature
         | 
| 163 48 |  | 
| 164 | 
            -
                 | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 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 | 
            -
             | 
| 179 | 
            -
                def feature_versions() release_migration.feature_versions end
         | 
| 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)
         | 
| 185 | 
            -
                  @name = name
         | 
| 186 | 
            -
                  @version = Migration.version(path)
         | 
| 187 | 
            -
                  @release_migration = ReleaseMigration.new(File.dirname(path))
         | 
| 188 | 
            -
                  @migrate_sql = File.join(path, MIGRATE_SQL)
         | 
| 189 | 
            -
                  @diff_sql = File.join(path, DIFF_SQL)
         | 
| 49 | 
            +
                def initialize(feature, base_version)
         | 
| 50 | 
            +
                  super(base_version, base_version)
         | 
| 51 | 
            +
                  @feature = feature
         | 
| 190 52 | 
             
                end
         | 
| 191 53 |  | 
| 192 | 
            -
                def  | 
| 193 | 
            -
             | 
| 194 | 
            -
                   | 
| 195 | 
            -
                   | 
| 54 | 
            +
                def self.load
         | 
| 55 | 
            +
                  migration_state = MigrationState.load
         | 
| 56 | 
            +
                  feature_state = FeatureState.load
         | 
| 57 | 
            +
                  FeatureMigration.new(feature_state.feature, migration_state.base_version)
         | 
| 196 58 | 
             
                end
         | 
| 197 59 |  | 
| 198 | 
            -
                def  | 
| 199 | 
            -
                def prepare() 
         | 
| 200 | 
            -
                  release_migration.present? || release_migration.prepare
         | 
| 60 | 
            +
                def create()
         | 
| 201 61 | 
             
                  super
         | 
| 202 | 
            -
                   | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 205 | 
            -
             | 
| 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) }
         | 
| 211 | 
            -
                end
         | 
| 212 | 
            -
             | 
| 213 | 
            -
                def self.release_files(path)
         | 
| 214 | 
            -
                  ReleaseMigration.files(path)
         | 
| 215 | 
            -
                end  
         | 
| 216 | 
            -
             | 
| 217 | 
            -
                def dump
         | 
| 218 | 
            -
                  super {
         | 
| 219 | 
            -
                    puts "name: #{name}"
         | 
| 220 | 
            -
                    puts "release_migration: #{path}"
         | 
| 221 | 
            -
                  }
         | 
| 62 | 
            +
                  files = Share.cp "migration", File.join(MIGRATION_DIR, feature)
         | 
| 63 | 
            +
                  state = FeatureState.write(feature: feature)
         | 
| 64 | 
            +
                  Git.add files, state.path
         | 
| 65 | 
            +
                  self
         | 
| 222 66 | 
             
                end
         | 
| 223 67 | 
             
              end
         | 
| 224 68 | 
             
            end
         | 
| 225 69 |  | 
| 226 70 |  | 
| 227 | 
            -
             | 
| 228 | 
            -
             | 
| 229 | 
            -
             | 
| 230 | 
            -
             |