Class: Kettle::Dev::VersionBump

Inherits:
Object
  • Object
show all
Defined in:
lib/kettle/dev/version_bump.rb

Overview

Reusable engine behind kettle-bump.

Constant Summary collapse

BUMP_TYPES =
%w[major minor patch pre].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, target_version:, current_version: nil, from_version: nil) ⇒ VersionBump

Returns a new instance of VersionBump.



148
149
150
151
152
153
# File 'lib/kettle/dev/version_bump.rb', line 148

def initialize(root:, target_version:, current_version: nil, from_version: nil)
  @root = root
  @current_version = current_version || Kettle::Dev::Versioning.detect_version(root)
  @target_version = self.class.resolve_target_version(target_version, @current_version)
  @from_version = self.class.validate_version(from_version) if from_version
end

Instance Attribute Details

#current_versionObject (readonly)

Returns the value of attribute current_version.



155
156
157
# File 'lib/kettle/dev/version_bump.rb', line 155

def current_version
  @current_version
end

#from_versionObject (readonly)

Returns the value of attribute from_version.



155
156
157
# File 'lib/kettle/dev/version_bump.rb', line 155

def from_version
  @from_version
end

#rootObject (readonly)

Returns the value of attribute root.



155
156
157
# File 'lib/kettle/dev/version_bump.rb', line 155

def root
  @root
end

#target_versionObject (readonly)

Returns the value of attribute target_version.



155
156
157
# File 'lib/kettle/dev/version_bump.rb', line 155

def target_version
  @target_version
end

Class Method Details

.bumped_prerelease_version(current_version) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/kettle/dev/version_bump.rb', line 53

def bumped_prerelease_version(current_version)
  version = Gem::Version.new(current_version)
  segments = version.segments
  prerelease_index = segments.index { |segment| !segment.is_a?(Integer) }
  unless prerelease_index
    raise Kettle::Dev::Error, "cannot pre-bump version without prerelease segment #{current_version.inspect}"
  end

  release_core = segments[0...prerelease_index].join(".")
  prerelease_suffix = prerelease_suffix_for(current_version, release_core)
  "#{release_core}.#{prerelease_suffix.next}"
end

.bumped_version(type, current_version) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/kettle/dev/version_bump.rb', line 25

def bumped_version(type, current_version)
  return bumped_prerelease_version(current_version) if type == "pre"

  version = Gem::Version.new(current_version)
  segments = version.segments
  return released_patch_version(segments) if type == "patch" && segments.any? { |segment| !segment.is_a?(Integer) }

  unless segments.all? { |segment| segment.is_a?(Integer) }
    raise Kettle::Dev::Error, "cannot #{type}-bump non-numeric version #{current_version.inspect}"
  end

  major, minor, patch = (segments + [0, 0, 0])[0, 3]
  case type
  when "major"
    "#{major + 1}.0.0"
  when "minor"
    "#{major}.#{minor + 1}.0"
  when "patch"
    "#{major}.#{minor}.#{patch + 1}"
  end
end

.each_node(root) ⇒ Object



106
107
108
109
110
111
112
113
114
115
# File 'lib/kettle/dev/version_bump.rb', line 106

def each_node(root)
  return enum_for(__method__, root) unless block_given?

  queue = [root]
  until queue.empty?
    node = queue.shift
    yield node
    queue.concat(node.child_nodes.compact) if node.respond_to?(:child_nodes)
  end
end

.file_edit(path, source, start_offset, end_offset, replacement) ⇒ Object



117
118
119
# File 'lib/kettle/dev/version_bump.rb', line 117

def file_edit(path, source, start_offset, end_offset, replacement)
  {path: path, source: source, start_offset: start_offset, end_offset: end_offset, replacement: replacement}
end

.parse_source(source, path) ⇒ Object

Raises:



90
91
92
93
94
95
96
# File 'lib/kettle/dev/version_bump.rb', line 90

def parse_source(source, path)
  require_prism
  parse_result = Prism.parse(source)
  raise Kettle::Dev::Error, "could not parse #{path}" unless parse_result.success?

  parse_result
end

.prerelease_suffix_for(current_version, release_core) ⇒ Object

Raises:



66
67
68
69
70
71
72
73
74
# File 'lib/kettle/dev/version_bump.rb', line 66

def prerelease_suffix_for(current_version, release_core)
  prefix = "#{release_core}."
  return string_tail(current_version, prefix.length) if current_version.start_with?(prefix)

  canonical_version = Gem::Version.new(current_version).to_s
  return string_tail(canonical_version, prefix.length) if canonical_version.start_with?(prefix)

  raise Kettle::Dev::Error, "cannot find prerelease segment in version #{current_version.inspect}"
end

.quote_like(original, value) ⇒ Object



121
122
123
124
# File 'lib/kettle/dev/version_bump.rb', line 121

def quote_like(original, value)
  quote = original.start_with?("'") ? "'" : '"'
  "#{quote}#{value}#{quote}"
end

.released_patch_version(segments) ⇒ Object



47
48
49
50
51
# File 'lib/kettle/dev/version_bump.rb', line 47

def released_patch_version(segments)
  release_segments = segments.take_while { |segment| segment.is_a?(Integer) }
  major, minor, patch = (release_segments + [0, 0, 0])[0, 3]
  "#{major}.#{minor}.#{patch}"
end

.replace_byte_range(source, start_offset, end_offset, replacement) ⇒ Object



141
142
143
144
145
# File 'lib/kettle/dev/version_bump.rb', line 141

def replace_byte_range(source, start_offset, end_offset, replacement)
  before = source.byteslice(0, start_offset) || +""
  after = source.byteslice(end_offset, source.bytesize - end_offset) || +""
  "#{before}#{replacement}#{after}"
end

.require_prismObject



98
99
100
101
102
103
104
# File 'lib/kettle/dev/version_bump.rb', line 98

def require_prism
  return if defined?(Prism)

  require "prism"
rescue LoadError => error
  raise Kettle::Dev::Error, "kettle-bump requires Prism; install the prism gem or run on Ruby 3.3+ (#{error.message})"
end

.resolve_target_version(target, current_version) ⇒ Object



10
11
12
13
14
15
16
17
# File 'lib/kettle/dev/version_bump.rb', line 10

def resolve_target_version(target, current_version)
  target = target.to_s
  if BUMP_TYPES.include?(target)
    bumped_version(target, current_version)
  else
    validate_version(target)
  end
end

.string_tail(value, offset) ⇒ Object



76
77
78
# File 'lib/kettle/dev/version_bump.rb', line 76

def string_tail(value, offset)
  value[offset, value.length - offset]
end

.validate_version(version) ⇒ Object



19
20
21
22
23
# File 'lib/kettle/dev/version_bump.rb', line 19

def validate_version(version)
  Gem::Version.new(version).to_s
rescue ArgumentError => error
  raise Kettle::Dev::Error, "invalid version #{version.inspect}: #{error.message}"
end

.version_string_node(source, path) ⇒ Object

Raises:



80
81
82
83
84
85
86
87
88
# File 'lib/kettle/dev/version_bump.rb', line 80

def version_string_node(source, path)
  parse_result = parse_source(source, path)
  constant = each_node(parse_result.value).find do |node|
    node.is_a?(Prism::ConstantWriteNode) && node.name == :VERSION && node.value.is_a?(Prism::StringNode)
  end
  raise Kettle::Dev::Error, "could not find string VERSION constant in #{path}" unless constant

  constant.value
end

.write_edits(edits) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/kettle/dev/version_bump.rb', line 126

def write_edits(edits)
  edits.group_by { |edit| edit.fetch(:path) }.each_value do |file_edits|
    source = file_edits.first.fetch(:source)
    file_edits.sort_by { |edit| -edit.fetch(:start_offset) }.each do |edit|
      source = replace_byte_range(
        source,
        edit.fetch(:start_offset),
        edit.fetch(:end_offset),
        edit.fetch(:replacement)
      )
    end
    File.write(file_edits.first.fetch(:path), source)
  end
end

Instance Method Details

#editsObject



157
158
159
160
# File 'lib/kettle/dev/version_bump.rb', line 157

def edits
  validate_from_version
  version_file_edits + gemspec_version_edits
end

#write!Object



162
163
164
# File 'lib/kettle/dev/version_bump.rb', line 162

def write!
  self.class.write_edits(edits)
end