ivar 0.2.0 → 0.4.6

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.augment-guidelines +5 -3
  3. data/.devcontainer/devcontainer.json +28 -20
  4. data/.devcontainer/post-create.sh +18 -0
  5. data/.editorconfig +35 -0
  6. data/.rubocop.yml +6 -0
  7. data/.standard.yml +1 -1
  8. data/.vscode/extensions.json +3 -1
  9. data/.vscode/launch.json +25 -0
  10. data/.vscode/settings.json +38 -2
  11. data/CHANGELOG.md +99 -1
  12. data/README.md +272 -207
  13. data/Rakefile +1 -1
  14. data/VERSION.md +46 -0
  15. data/examples/check_all_block_example.rb +84 -0
  16. data/examples/check_all_example.rb +42 -0
  17. data/examples/inheritance_with_kwarg_init.rb +156 -0
  18. data/examples/inheritance_with_positional_init.rb +142 -0
  19. data/examples/mixed_positional_and_kwarg_init.rb +125 -0
  20. data/examples/require_check_all_example.rb +23 -0
  21. data/examples/sandwich_inheritance.rb +1 -1
  22. data/examples/sandwich_with_accessors.rb +78 -0
  23. data/examples/sandwich_with_block_values.rb +54 -0
  24. data/examples/sandwich_with_checked.rb +0 -1
  25. data/examples/sandwich_with_checked_once.rb +0 -1
  26. data/examples/sandwich_with_initial_values.rb +52 -0
  27. data/examples/sandwich_with_ivar_block.rb +6 -9
  28. data/examples/sandwich_with_ivar_macro.rb +4 -4
  29. data/examples/sandwich_with_kwarg_init.rb +78 -0
  30. data/examples/sandwich_with_positional_init.rb +50 -0
  31. data/examples/sandwich_with_shared_values.rb +54 -0
  32. data/hooks/README.md +42 -0
  33. data/hooks/install.sh +12 -0
  34. data/hooks/pre-commit +54 -0
  35. data/lib/ivar/check_all.rb +7 -0
  36. data/lib/ivar/check_all_manager.rb +72 -0
  37. data/lib/ivar/check_policy.rb +29 -0
  38. data/lib/ivar/checked/class_methods.rb +19 -0
  39. data/lib/ivar/checked/instance_methods.rb +35 -0
  40. data/lib/ivar/checked.rb +17 -24
  41. data/lib/ivar/declaration.rb +30 -0
  42. data/lib/ivar/explicit_declaration.rb +56 -0
  43. data/lib/ivar/explicit_keyword_declaration.rb +24 -0
  44. data/lib/ivar/explicit_positional_declaration.rb +19 -0
  45. data/lib/ivar/macros.rb +48 -111
  46. data/lib/ivar/manifest.rb +124 -0
  47. data/lib/ivar/policies.rb +13 -1
  48. data/lib/ivar/project_root.rb +59 -0
  49. data/lib/ivar/targeted_prism_analysis.rb +144 -0
  50. data/lib/ivar/validation.rb +6 -29
  51. data/lib/ivar/version.rb +1 -1
  52. data/lib/ivar.rb +141 -9
  53. data/script/console +11 -0
  54. data/script/de-lint +2 -0
  55. data/script/de-lint-unsafe +2 -0
  56. data/script/lint +2 -0
  57. data/script/release +213 -0
  58. data/script/setup +8 -0
  59. data/script/test +2 -0
  60. metadata +46 -8
  61. data/examples/sandwich_with_kwarg.rb +0 -45
  62. data/ivar.gemspec +0 -49
  63. data/lib/ivar/auto_check.rb +0 -77
  64. data/lib/ivar/prism_analysis.rb +0 -102
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Ivar
6
+ # Analyzes a class to find instance variable references in specific instance methods
7
+ # Unlike PrismAnalysis, this targets only the class's own methods (not inherited)
8
+ # and precisely locates instance variable references within each method definition
9
+ class TargetedPrismAnalysis
10
+ attr_reader :ivars, :references
11
+
12
+ def initialize(klass)
13
+ @klass = klass
14
+ @references = []
15
+ @method_locations = {}
16
+ collect_method_locations
17
+ analyze_methods
18
+ @ivars = unique_ivar_names
19
+ end
20
+
21
+ # Returns a list of hashes each representing a code reference to an ivar
22
+ # Each hash includes var name, path, line number, and column number
23
+ def ivar_references
24
+ @references
25
+ end
26
+
27
+ private
28
+
29
+ def unique_ivar_names
30
+ @references.map { |ref| ref[:name] }.uniq.sort
31
+ end
32
+
33
+ def collect_method_locations
34
+ # Get all instance methods defined directly on this class (not inherited)
35
+ instance_methods = @klass.instance_methods(false) | @klass.private_instance_methods(false)
36
+ instance_methods.each do |method_name|
37
+ # Try to get the method from the stash first, then fall back to the current method
38
+ method_obj = Ivar.get_stashed_method(@klass, method_name) || @klass.instance_method(method_name)
39
+ next unless method_obj.source_location
40
+
41
+ file_path, line_number = method_obj.source_location
42
+ @method_locations[method_name] = {path: file_path, line: line_number}
43
+ end
44
+ end
45
+
46
+ def analyze_methods
47
+ # Group methods by file to avoid parsing the same file multiple times
48
+ methods_by_file = @method_locations.group_by { |_, location| location[:path] }
49
+
50
+ methods_by_file.each do |file_path, methods_in_file|
51
+ code = File.read(file_path)
52
+ result = Prism.parse(code)
53
+
54
+ methods_in_file.each do |method_name, location|
55
+ visitor = MethodTargetedInstanceVariableReferenceVisitor.new(
56
+ file_path,
57
+ method_name,
58
+ location[:line]
59
+ )
60
+
61
+ result.value.accept(visitor)
62
+ @references.concat(visitor.references)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # Visitor that collects instance variable references within a specific method definition
69
+ class MethodTargetedInstanceVariableReferenceVisitor < Prism::Visitor
70
+ attr_reader :references
71
+
72
+ def initialize(file_path, target_method_name, target_line)
73
+ super()
74
+ @file_path = file_path
75
+ @target_method_name = target_method_name
76
+ @target_line = target_line
77
+ @references = []
78
+ @in_target_method = false
79
+ end
80
+
81
+ # Only visit the method definition we're targeting
82
+ def visit_def_node(node)
83
+ # Check if this is our target method
84
+ if node.name.to_sym == @target_method_name && node.location.start_line == @target_line
85
+ # Found our target method, now collect all instance variable references within it
86
+ collector = IvarCollector.new(@file_path, @target_method_name)
87
+ node.body&.accept(collector)
88
+ @references = collector.references
89
+ false
90
+ else
91
+ # Sometimes methods are found inside other methods...
92
+ node.body&.accept(self)
93
+ true
94
+ end
95
+ end
96
+ end
97
+
98
+ # Helper visitor that collects all instance variable references
99
+ class IvarCollector < Prism::Visitor
100
+ attr_reader :references
101
+
102
+ def initialize(file_path, method_name)
103
+ super()
104
+ @file_path = file_path
105
+ @method_name = method_name
106
+ @references = []
107
+ end
108
+
109
+ def visit_instance_variable_read_node(node)
110
+ add_reference(node)
111
+ true
112
+ end
113
+
114
+ def visit_instance_variable_write_node(node)
115
+ add_reference(node)
116
+ true
117
+ end
118
+
119
+ def visit_instance_variable_operator_write_node(node)
120
+ add_reference(node)
121
+ true
122
+ end
123
+
124
+ def visit_instance_variable_target_node(node)
125
+ add_reference(node)
126
+ true
127
+ end
128
+
129
+ private
130
+
131
+ def add_reference(node)
132
+ location = node.location
133
+ reference = {
134
+ name: node.name.to_sym,
135
+ path: @file_path,
136
+ line: location.start_line,
137
+ column: location.start_column,
138
+ method: @method_name
139
+ }
140
+
141
+ @references << reference
142
+ end
143
+ end
144
+ end
@@ -9,46 +9,23 @@ module Ivar
9
9
  # @param add [Array<Symbol>] Additional instance variables to allow
10
10
  # @param policy [Symbol, Policy] The policy to use for handling unknown variables
11
11
  def check_ivars(add: [], policy: nil)
12
- # Get the policy to use
13
12
  policy ||= get_check_policy
14
-
15
- # Get the class analysis from the cache
16
- analysis = Ivar.get_analysis(self.class)
17
-
18
- # Get all instance variables defined in the current object
19
- # These are the ones the user has explicitly defined before calling check_ivars
20
- defined_ivars = instance_variables.map(&:to_sym)
21
-
22
- # Add any additional allowed variables
23
- allowed_ivars = defined_ivars + add
24
-
25
- # Get all instance variable references from the analysis
26
- # This includes location information for each reference
27
- references = analysis.ivar_references
28
-
29
- # Find references to unknown variables (those not in allowed_ivars)
30
- unknown_refs = references.reject { |ref| allowed_ivars.include?(ref[:name]) }
31
-
32
- # Handle unknown variables according to the policy
13
+ analyses = Ivar.get_ancestral_analyses(self.class)
14
+ manifest = Ivar.get_or_create_manifest(self.class)
15
+ declared_ivars = manifest.all_declarations.map(&:name)
16
+ allowed_ivars = (Ivar.known_internal_ivars | instance_variables | declared_ivars | add).uniq
17
+ instance_refs = analyses.flat_map(&:references)
18
+ unknown_refs = instance_refs.reject { |ref| allowed_ivars.include?(ref[:name]) }
33
19
  policy_instance = Ivar.get_policy(policy)
34
20
  policy_instance.handle_unknown_ivars(unknown_refs, self.class, allowed_ivars)
35
21
  end
36
22
 
37
- # For backward compatibility - delegates to check_ivars with warn_once policy
38
- # @param add [Array<Symbol>] Additional instance variables to allow
39
- def check_ivars_once(add: [])
40
- check_ivars(add: add, policy: :warn_once)
41
- end
42
-
43
23
  private
44
24
 
45
25
  # Get the check policy for this instance
46
26
  # @return [Symbol, Policy] The check policy
47
27
  def get_check_policy
48
- # If the class has an ivar_check_policy method, use that
49
28
  return self.class.ivar_check_policy if self.class.respond_to?(:ivar_check_policy)
50
-
51
- # Otherwise, use the global default
52
29
  Ivar.check_policy
53
30
  end
54
31
  end
data/lib/ivar/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ivar
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.6"
5
5
  end
data/lib/ivar.rb CHANGED
@@ -1,42 +1,124 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ivar/version"
4
- require_relative "ivar/prism_analysis"
5
4
  require_relative "ivar/policies"
6
5
  require_relative "ivar/validation"
7
6
  require_relative "ivar/macros"
8
- require_relative "ivar/auto_check"
7
+ require_relative "ivar/project_root"
8
+ require_relative "ivar/check_all_manager"
9
+ require_relative "ivar/check_policy"
10
+ require_relative "ivar/checked"
11
+ require_relative "ivar/manifest"
12
+ require_relative "ivar/targeted_prism_analysis"
9
13
  require "prism"
10
14
  require "did_you_mean"
15
+ require "pathname"
11
16
 
12
17
  module Ivar
13
18
  @analysis_cache = {}
14
19
  @checked_classes = {}
15
- @default_check_policy = :warn
20
+ @default_check_policy = :warn_once
21
+ @manifest_registry = {}
22
+ @project_root = nil
23
+ MUTEX = Mutex.new
24
+ PROJECT_ROOT_FINDER = ProjectRoot.new
25
+ CHECK_ALL_MANAGER = CheckAllManager.new
26
+
27
+ # Pattern for internal instance variables
28
+ INTERNAL_IVAR_PREFIX = "@__ivar_"
29
+
30
+ # Checks if an instance variable name is an internal variable
31
+ # @param ivar_name [Symbol, String] The instance variable name to check
32
+ # @return [Boolean] Whether the variable is an internal variable
33
+ def self.internal_ivar?(ivar_name)
34
+ ivar_name.to_s.start_with?(INTERNAL_IVAR_PREFIX)
35
+ end
36
+
37
+ # Returns a list of known internal instance variables
38
+ # @return [Array<Symbol>] List of known internal instance variables
39
+ def self.known_internal_ivars
40
+ [
41
+ :@__ivar_check_policy,
42
+ :@__ivar_initialized_vars,
43
+ :@__ivar_method_impl_stash,
44
+ :@__ivar_skip_init
45
+ ]
46
+ end
47
+
48
+ def self.get_ancestral_analyses(klass)
49
+ klass
50
+ .ancestors.filter_map { |ancestor| maybe_get_analysis(ancestor) }
51
+ .reverse
52
+ end
53
+
54
+ def self.maybe_get_analysis(klass)
55
+ if klass.include?(Validation)
56
+ get_analysis(klass)
57
+ end
58
+ end
16
59
 
17
60
  # Returns a cached analysis for the given class or module
18
61
  # Creates a new analysis if one doesn't exist in the cache
62
+ # Thread-safe: Multiple readers are allowed, but writers block all other access
19
63
  def self.get_analysis(klass)
20
- @analysis_cache[klass] ||= PrismAnalysis.new(klass)
64
+ return @analysis_cache[klass] if @analysis_cache.key?(klass)
65
+
66
+ MUTEX.synchronize do
67
+ @analysis_cache[klass] ||= TargetedPrismAnalysis.new(klass)
68
+ end
21
69
  end
22
70
 
23
71
  # Checks if a class has been validated already
24
72
  # @param klass [Class] The class to check
25
73
  # @return [Boolean] Whether the class has been validated
74
+ # Thread-safe: Read-only operation
26
75
  def self.class_checked?(klass)
27
- @checked_classes.key?(klass)
76
+ MUTEX.synchronize { @checked_classes.key?(klass) }
28
77
  end
29
78
 
30
79
  # Marks a class as having been checked
31
80
  # @param klass [Class] The class to mark as checked
81
+ # Thread-safe: Write operation protected by mutex
32
82
  def self.mark_class_checked(klass)
33
- @checked_classes[klass] = true
83
+ MUTEX.synchronize { @checked_classes[klass] = true }
34
84
  end
35
85
 
36
86
  # For testing purposes - allows clearing the cache
87
+ # Thread-safe: Write operation protected by mutex
37
88
  def self.clear_analysis_cache
38
- @analysis_cache.clear
39
- @checked_classes.clear
89
+ MUTEX.synchronize do
90
+ @analysis_cache.clear
91
+ @checked_classes.clear
92
+ @manifest_registry.clear
93
+ end
94
+ PROJECT_ROOT_FINDER.clear_cache
95
+ end
96
+
97
+ # Get or create a manifest for a class or module
98
+ # @param klass [Class, Module] The class or module to get a manifest for
99
+ # @param create [Boolean] Whether to create a new manifest if one doesn't exist
100
+ # @return [Manifest, nil] The manifest for the class or module, or nil if not found and create_if_missing is false
101
+ def self.get_manifest(klass, create: true)
102
+ return @manifest_registry[klass] if @manifest_registry.key?(klass)
103
+ return nil unless create
104
+
105
+ MUTEX.synchronize do
106
+ @manifest_registry[klass] ||= Manifest.new(klass)
107
+ end
108
+ end
109
+
110
+ # Alias for get_manifest that makes it clearer that it may create a manifest
111
+ # @param klass [Class, Module] The class or module to get a manifest for
112
+ # @return [Manifest] The manifest for the class or module
113
+ def self.get_or_create_manifest(klass)
114
+ get_manifest(klass, create: true)
115
+ end
116
+
117
+ # Check if a manifest exists for a class or module
118
+ # @param klass [Class, Module] The class or module to check
119
+ # @return [Boolean] Whether a manifest exists for the class or module
120
+ def self.manifest_exists?(klass)
121
+ @manifest_registry.key?(klass)
40
122
  end
41
123
 
42
124
  # Get the default check policy
@@ -48,6 +130,56 @@ module Ivar
48
130
  # Set the default check policy
49
131
  # @param policy [Symbol, Policy] The default check policy
50
132
  def self.check_policy=(policy)
51
- @default_check_policy = policy
133
+ MUTEX.synchronize { @default_check_policy = policy }
134
+ end
135
+
136
+ def self.project_root=(explicit_root)
137
+ @project_root = explicit_root
138
+ end
139
+
140
+ # Determines the project root directory based on the caller's location
141
+ # Delegates to ProjectRoot class
142
+ # @param caller_location [String, nil] Optional file path to start from (defaults to caller's location)
143
+ # @return [String] The absolute path to the project root directory
144
+ def self.project_root(caller_location = nil)
145
+ @project_root ||= PROJECT_ROOT_FINDER.find(caller_location)
146
+ end
147
+
148
+ # Enables automatic inclusion of Ivar::Checked in all classes and modules
149
+ # defined within the project root.
150
+ #
151
+ # @param block [Proc] Optional block. If provided, auto-checking is only active
152
+ # for the duration of the block. Otherwise, it remains active indefinitely.
153
+ # @return [void]
154
+ def self.check_all(&block)
155
+ root = project_root
156
+ CHECK_ALL_MANAGER.enable(root, &block)
157
+ end
158
+
159
+ # Disables automatic inclusion of Ivar::Checked in classes and modules.
160
+ # @return [void]
161
+ def self.disable_check_all
162
+ CHECK_ALL_MANAGER.disable
163
+ end
164
+
165
+ # Gets a method from the stash or returns nil if not found
166
+ # @param klass [Class] The class that owns the method
167
+ # @param method_name [Symbol] The name of the method to retrieve
168
+ # @return [UnboundMethod, nil] The stashed method or nil if not found
169
+ def self.get_stashed_method(klass, method_name)
170
+ (klass.instance_variable_get(:@__ivar_method_impl_stash) || {})[method_name]
171
+ end
172
+
173
+ # Stashes a method implementation for a class
174
+ # @param klass [Class] The class that owns the method
175
+ # @param method_name [Symbol] The name of the method to stash
176
+ # @return [UnboundMethod, nil] The stashed method or nil if the method doesn't exist
177
+ def self.stash_method(klass, method_name)
178
+ return nil unless klass.method_defined?(method_name) || klass.private_method_defined?(method_name)
179
+
180
+ method_impl = klass.instance_method(method_name)
181
+ stash = klass.instance_variable_get(:@__ivar_method_impl_stash) ||
182
+ klass.instance_variable_set(:@__ivar_method_impl_stash, {})
183
+ stash[method_name] = method_impl
52
184
  end
53
185
  end
data/script/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ivar"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require "irb"
11
+ IRB.start(__FILE__)
data/script/de-lint ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ STANDARDOPTS="--fix $*" bundle exec rake standard
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ STANDARDOPTS="--fix-unsafely $*" bundle exec rake standard
data/script/lint ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ STANDARDOPTS="$*" bundle exec rake standard
data/script/release ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This script helps with releasing a new version of the gem
5
+ # Usage: script/release [major|minor|patch] [options]
6
+ #
7
+ # Options:
8
+ # --yes, -y Skip confirmation prompt
9
+ # --no-push Skip pushing changes to remote repository
10
+
11
+ require "bundler/gem_tasks"
12
+ require_relative "../lib/ivar/version"
13
+
14
+ def error(message)
15
+ puts "\e[31mError: #{message}\e[0m"
16
+ exit 1
17
+ end
18
+
19
+ def success(message)
20
+ puts "\e[32m#{message}\e[0m"
21
+ end
22
+
23
+ def info(message)
24
+ puts "\e[34m#{message}\e[0m"
25
+ end
26
+
27
+ def get_new_version(current_version, bump_type)
28
+ major, minor, patch = current_version.split(".").map(&:to_i)
29
+
30
+ case bump_type
31
+ when "major"
32
+ "#{major + 1}.0.0"
33
+ when "minor"
34
+ "#{major}.#{minor + 1}.0"
35
+ when "patch"
36
+ "#{major}.#{minor}.#{patch + 1}"
37
+ else
38
+ error "Invalid bump type. Use 'major', 'minor', or 'patch'."
39
+ end
40
+ end
41
+
42
+ def update_version_file(new_version)
43
+ version_file_path = "lib/ivar/version.rb"
44
+ version_content = File.read(version_file_path)
45
+ updated_content = version_content.gsub(/VERSION = "[0-9]+\.[0-9]+\.[0-9]+"/, "VERSION = \"#{new_version}\"")
46
+ File.write(version_file_path, updated_content)
47
+ end
48
+
49
+ def update_changelog(new_version)
50
+ changelog_path = "CHANGELOG.md"
51
+ changelog_content = File.read(changelog_path)
52
+
53
+ # Check if there are unreleased changes
54
+ unless changelog_content.include?("## [Unreleased]")
55
+ error "No unreleased changes found in CHANGELOG.md. Add changes before releasing."
56
+ end
57
+
58
+ # Update the changelog with the new version
59
+ today = Time.now.strftime("%Y-%m-%d")
60
+ updated_content = changelog_content.gsub(
61
+ "## [Unreleased]",
62
+ "## [Unreleased]\n\n## [#{new_version}] - #{today}"
63
+ )
64
+
65
+ File.write(changelog_path, updated_content)
66
+ end
67
+
68
+ def run_tests
69
+ info "Running tests..."
70
+ system("bundle exec rake test") || error("Tests failed. Fix the tests before releasing.")
71
+ end
72
+
73
+ def run_linter
74
+ info "Running linter..."
75
+ system("bundle exec rake standard") || error("Linter found issues. Fix them before releasing.")
76
+ end
77
+
78
+ def clean_build_artifacts
79
+ info "Cleaning build artifacts..."
80
+ system("bundle exec rake clean clobber")
81
+ # Also remove any stray .gem files in the project root
82
+ Dir.glob("*.gem").each do |gem_file|
83
+ info "Removing stray gem file: #{gem_file}"
84
+ File.delete(gem_file)
85
+ end
86
+ end
87
+
88
+ def check_for_uncommitted_changes
89
+ info "Checking for uncommitted changes..."
90
+ uncommitted_changes = `git status --porcelain`.strip
91
+
92
+ if uncommitted_changes.empty?
93
+ info "No uncommitted changes detected."
94
+ false
95
+ else
96
+ info "Uncommitted changes detected:"
97
+ puts uncommitted_changes
98
+ true
99
+ end
100
+ end
101
+
102
+ def commit_remaining_changes(new_version)
103
+ info "Committing remaining changes after release process..."
104
+ system("git add --all")
105
+ system("git commit -m \"Post-release cleanup for v#{new_version}\"")
106
+ info "Remaining changes committed."
107
+ end
108
+
109
+ def push_changes_and_tag(new_version)
110
+ # Check for any uncommitted changes before pushing
111
+ has_uncommitted_changes = check_for_uncommitted_changes
112
+
113
+ # If there are uncommitted changes, commit them
114
+ if has_uncommitted_changes
115
+ commit_remaining_changes(new_version)
116
+ end
117
+
118
+ info "Pushing changes to remote repository..."
119
+ system("git push origin main") || error("Failed to push changes to remote repository.")
120
+
121
+ info "Pushing tag v#{new_version} to remote repository..."
122
+ system("git push origin v#{new_version}") || error("Failed to push tag to remote repository.")
123
+
124
+ success "Changes and tag pushed successfully!"
125
+ end
126
+
127
+ def update_gemfile_lock
128
+ info "Updating Gemfile.lock with new version..."
129
+ system("bundle install") || error("Failed to update Gemfile.lock. Run 'bundle install' manually and try again.")
130
+ info "Gemfile.lock updated successfully."
131
+ end
132
+
133
+ def commit_and_tag(new_version, skip_push = false)
134
+ info "Committing version bump..."
135
+
136
+ # Add all relevant files to staging
137
+ system("git add lib/ivar/version.rb CHANGELOG.md Gemfile.lock")
138
+
139
+ # Commit the changes
140
+ system("git commit -m \"Bump version to #{new_version}\"")
141
+
142
+ info "Creating tag v#{new_version}..."
143
+ system("git tag -a v#{new_version} -m \"Version #{new_version}\"")
144
+
145
+ if skip_push
146
+ info "Skipping push to remote repository."
147
+ info "To push the new version manually, run:"
148
+ puts " git push origin main && git push origin v#{new_version}"
149
+ else
150
+ push_changes_and_tag(new_version)
151
+ end
152
+ end
153
+
154
+ # Main script
155
+ error "Please specify a version bump type: major, minor, or patch" if ARGV.empty?
156
+
157
+ # Parse arguments
158
+ args = ARGV.dup
159
+ skip_confirmation = args.delete("--yes") || args.delete("-y")
160
+ skip_push = args.delete("--no-push")
161
+ bump_type = args[0].downcase if args[0]
162
+
163
+ error "Please specify a version bump type: major, minor, or patch" unless bump_type
164
+
165
+ current_version = Ivar::VERSION
166
+ new_version = get_new_version(current_version, bump_type)
167
+
168
+ info "Current version: #{current_version}"
169
+ info "New version: #{new_version}"
170
+
171
+ # Skip confirmation if --yes/-y option is provided
172
+ confirmation = "y" if skip_confirmation
173
+
174
+ unless confirmation
175
+ puts "Continue? (y/n)"
176
+ confirmation = $stdin.gets.chomp.downcase
177
+ end
178
+
179
+ if confirmation == "y"
180
+ clean_build_artifacts
181
+ run_tests
182
+ run_linter
183
+ update_version_file(new_version)
184
+ update_changelog(new_version)
185
+ update_gemfile_lock
186
+ commit_and_tag(new_version, skip_push)
187
+ success "Version bumped to #{new_version}!"
188
+
189
+ if skip_push
190
+ success "Remember to push changes manually to trigger the release workflow."
191
+ else
192
+ success "Release workflow triggered!"
193
+
194
+ # Final check for any remaining uncommitted changes
195
+ if check_for_uncommitted_changes
196
+ info "There are still uncommitted changes after the release process."
197
+ puts "Would you like to commit and push these changes? (y/n)"
198
+ cleanup_confirmation = $stdin.gets.chomp.downcase
199
+
200
+ if cleanup_confirmation == "y"
201
+ commit_remaining_changes(new_version)
202
+ system("git push origin main") || error("Failed to push cleanup changes.")
203
+ success "Post-release cleanup completed and pushed successfully!"
204
+ else
205
+ info "Uncommitted changes left in working directory."
206
+ end
207
+ else
208
+ success "Working directory is clean. Release completed successfully!"
209
+ end
210
+ end
211
+ else
212
+ info "Release cancelled."
213
+ end
data/script/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/script/test ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ TESTOPTS="$*" bundle exec rake test