-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Git hook to rollback phantom migrations on branch switch (#110)
- Loading branch information
1 parent
30ec5ee
commit a8b4d93
Showing
5 changed files
with
390 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# frozen_string_literal: true | ||
|
||
require "fileutils" | ||
|
||
module ActualDbSchema | ||
# Handles the installation of a git post-checkout hook that rolls back phantom migrations when switching branches | ||
class GitHooks # rubocop:disable Metrics/ClassLength | ||
include ActualDbSchema::OutputFormatter | ||
|
||
POST_CHECKOUT_MARKER_START = "# >>> BEGIN ACTUAL_DB_SCHEMA" | ||
POST_CHECKOUT_MARKER_END = "# <<< END ACTUAL_DB_SCHEMA" | ||
|
||
POST_CHECKOUT_HOOK_ROLLBACK = <<~BASH | ||
#{POST_CHECKOUT_MARKER_START} | ||
# ActualDbSchema post-checkout hook (ROLLBACK) | ||
# Runs db:rollback_branches on branch checkout. | ||
if [ -f ./bin/rails ]; then | ||
if [ -n "$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" ]; then | ||
GIT_HOOKS_ENABLED="$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" | ||
else | ||
GIT_HOOKS_ENABLED=$(./bin/rails runner "puts ActualDbSchema.config[:git_hooks_enabled]" 2>/dev/null) | ||
fi | ||
if [ "$GIT_HOOKS_ENABLED" == "true" ]; then | ||
./bin/rails db:rollback_branches | ||
fi | ||
fi | ||
#{POST_CHECKOUT_MARKER_END} | ||
BASH | ||
|
||
POST_CHECKOUT_HOOK_MIGRATE = <<~BASH | ||
#{POST_CHECKOUT_MARKER_START} | ||
# ActualDbSchema post-checkout hook (MIGRATE) | ||
# Runs db:migrate on branch checkout. | ||
if [ -f ./bin/rails ]; then | ||
if [ -n "$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" ]; then | ||
GIT_HOOKS_ENABLED="$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" | ||
else | ||
GIT_HOOKS_ENABLED=$(./bin/rails runner "puts ActualDbSchema.config[:git_hooks_enabled]" 2>/dev/null) | ||
fi | ||
if [ "$GIT_HOOKS_ENABLED" == "true" ]; then | ||
./bin/rails db:migrate | ||
fi | ||
fi | ||
#{POST_CHECKOUT_MARKER_END} | ||
BASH | ||
|
||
def initialize(strategy: :rollback) | ||
@strategy = strategy | ||
end | ||
|
||
def install_post_checkout_hook | ||
return unless git_hooks_enabled? | ||
return unless hooks_directory_present? | ||
|
||
if File.exist?(hook_path) | ||
handle_existing_hook | ||
else | ||
create_new_hook | ||
end | ||
end | ||
|
||
private | ||
|
||
def hook_code | ||
@strategy == :migrate ? POST_CHECKOUT_HOOK_MIGRATE : POST_CHECKOUT_HOOK_ROLLBACK | ||
end | ||
|
||
def hooks_dir | ||
@hooks_dir ||= Rails.root.join(".git", "hooks") | ||
end | ||
|
||
def hook_path | ||
@hook_path ||= hooks_dir.join("post-checkout") | ||
end | ||
|
||
def git_hooks_enabled? | ||
return true if ActualDbSchema.config[:git_hooks_enabled] | ||
|
||
puts colorize("[ActualDbSchema] Git hooks are disabled in configuration. Skipping installation.", :gray) | ||
end | ||
|
||
def hooks_directory_present? | ||
return true if Dir.exist?(hooks_dir) | ||
|
||
puts colorize("[ActualDbSchema] .git/hooks directory not found. Please ensure this is a Git repository.", :gray) | ||
end | ||
|
||
def handle_existing_hook | ||
return update_hook if markers_exist? | ||
return install_hook if safe_install? | ||
|
||
show_manual_install_instructions | ||
end | ||
|
||
def create_new_hook | ||
contents = <<~BASH | ||
#!/usr/bin/env bash | ||
#{hook_code} | ||
BASH | ||
|
||
write_hook_file(contents) | ||
print_success | ||
end | ||
|
||
def markers_exist? | ||
contents = File.read(hook_path) | ||
contents.include?(POST_CHECKOUT_MARKER_START) && contents.include?(POST_CHECKOUT_MARKER_END) | ||
end | ||
|
||
def update_hook | ||
contents = File.read(hook_path) | ||
new_contents = replace_marker_contents(contents) | ||
|
||
if new_contents == contents | ||
message = "[ActualDbSchema] post-checkout git hook already contains the necessary code. Nothing to update." | ||
puts colorize(message, :gray) | ||
else | ||
write_hook_file(new_contents) | ||
puts colorize("[ActualDbSchema] post-checkout git hook updated successfully at #{hook_path}", :green) | ||
end | ||
end | ||
|
||
def replace_marker_contents(contents) | ||
contents.gsub( | ||
/#{Regexp.quote(POST_CHECKOUT_MARKER_START)}.*#{Regexp.quote(POST_CHECKOUT_MARKER_END)}/m, | ||
hook_code.strip | ||
) | ||
end | ||
|
||
def safe_install? | ||
puts colorize("[ActualDbSchema] A post-checkout hook already exists at #{hook_path}.", :gray) | ||
puts "Overwrite the existing hook at #{hook_path}? [y,n] " | ||
|
||
answer = $stdin.gets.chomp.downcase | ||
answer.start_with?("y") | ||
end | ||
|
||
def install_hook | ||
contents = File.read(hook_path) | ||
new_contents = <<~BASH | ||
#{contents.rstrip} | ||
#{hook_code} | ||
BASH | ||
|
||
write_hook_file(new_contents) | ||
print_success | ||
end | ||
|
||
def show_manual_install_instructions | ||
puts colorize("[ActualDbSchema] You can follow these steps to manually install the hook:", :yellow) | ||
puts <<~MSG | ||
1. Open the existing post-checkout hook at: | ||
#{hook_path} | ||
2. Insert the following lines into that file (preferably at the end or in a relevant section). | ||
Make sure you include the #{POST_CHECKOUT_MARKER_START} and #{POST_CHECKOUT_MARKER_END} lines: | ||
#{hook_code} | ||
3. Ensure the post-checkout file is executable: | ||
chmod +x #{hook_path} | ||
4. Done! Now when you switch branches, phantom migrations will be rolled back automatically (if enabled). | ||
MSG | ||
end | ||
|
||
def write_hook_file(contents) | ||
File.open(hook_path, "w") { |file| file.write(contents) } | ||
FileUtils.chmod("+x", hook_path) | ||
end | ||
|
||
def print_success | ||
puts colorize("[ActualDbSchema] post-checkout git hook installed successfully at #{hook_path}", :green) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# frozen_string_literal: true | ||
|
||
namespace :actual_db_schema do | ||
desc "Install ActualDbSchema post-checkout git hook that rolls back phantom migrations when switching branches." | ||
task :install_git_hooks do | ||
extend ActualDbSchema::OutputFormatter | ||
|
||
puts "Which Git hook strategy would you like to install? [1, 2, 3]" | ||
puts " 1) Rollback phantom migrations (db:rollback_branches)" | ||
puts " 2) Migrate up to latest (db:migrate)" | ||
puts " 3) No hook installation (skip)" | ||
answer = $stdin.gets.chomp | ||
|
||
strategy = | ||
case answer | ||
when "1" then :rollback | ||
when "2" then :migrate | ||
else | ||
:none | ||
end | ||
|
||
if strategy == :none | ||
puts colorize("[ActualDbSchema] Skipping git hook installation.", :gray) | ||
else | ||
ActualDbSchema::GitHooks.new(strategy: strategy).install_post_checkout_hook | ||
end | ||
end | ||
end |
Oops, something went wrong.